@smoothbricks/cli 0.1.1 → 0.3.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 (131) hide show
  1. package/README.md +161 -32
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +110 -10
  4. package/dist/devenv/index.d.ts +10 -0
  5. package/dist/devenv/index.d.ts.map +1 -0
  6. package/dist/devenv/index.js +27 -0
  7. package/dist/generate/index.d.ts +20 -0
  8. package/dist/generate/index.d.ts.map +1 -0
  9. package/dist/generate/index.js +47 -0
  10. package/dist/lib/cli-package.d.ts +4 -0
  11. package/dist/lib/cli-package.d.ts.map +1 -0
  12. package/dist/lib/cli-package.js +15 -0
  13. package/dist/lib/run.d.ts +6 -0
  14. package/dist/lib/run.d.ts.map +1 -1
  15. package/dist/lib/run.js +32 -0
  16. package/dist/lib/workspace.d.ts +25 -1
  17. package/dist/lib/workspace.d.ts.map +1 -1
  18. package/dist/lib/workspace.js +79 -17
  19. package/dist/monorepo/commit-msg.d.ts +8 -1
  20. package/dist/monorepo/commit-msg.d.ts.map +1 -1
  21. package/dist/monorepo/commit-msg.js +85 -7
  22. package/dist/monorepo/index.d.ts +19 -2
  23. package/dist/monorepo/index.d.ts.map +1 -1
  24. package/dist/monorepo/index.js +104 -16
  25. package/dist/monorepo/lockfile.d.ts.map +1 -1
  26. package/dist/monorepo/lockfile.js +9 -0
  27. package/dist/monorepo/managed-files.js +1 -1
  28. package/dist/monorepo/nx-sync.d.ts +2 -1
  29. package/dist/monorepo/nx-sync.d.ts.map +1 -1
  30. package/dist/monorepo/nx-sync.js +8 -2
  31. package/dist/monorepo/package-hygiene.d.ts +6 -2
  32. package/dist/monorepo/package-hygiene.d.ts.map +1 -1
  33. package/dist/monorepo/package-hygiene.js +12 -4
  34. package/dist/monorepo/package-policy.d.ts +13 -4
  35. package/dist/monorepo/package-policy.d.ts.map +1 -1
  36. package/dist/monorepo/package-policy.js +208 -113
  37. package/dist/monorepo/packed-package.d.ts +3 -0
  38. package/dist/monorepo/packed-package.d.ts.map +1 -1
  39. package/dist/monorepo/packed-package.js +68 -36
  40. package/dist/monorepo/packs/index.d.ts +20 -1
  41. package/dist/monorepo/packs/index.d.ts.map +1 -1
  42. package/dist/monorepo/packs/index.js +238 -19
  43. package/dist/monorepo/publish-workflow.d.ts +0 -4
  44. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  45. package/dist/monorepo/publish-workflow.js +7 -15
  46. package/dist/monorepo/tool-validation.d.ts +22 -0
  47. package/dist/monorepo/tool-validation.d.ts.map +1 -0
  48. package/dist/monorepo/tool-validation.js +399 -0
  49. package/dist/nx/index.d.ts +25 -0
  50. package/dist/nx/index.d.ts.map +1 -0
  51. package/dist/nx/index.js +112 -0
  52. package/dist/release/bootstrap-npm-packages.d.ts +19 -0
  53. package/dist/release/bootstrap-npm-packages.d.ts.map +1 -0
  54. package/dist/release/bootstrap-npm-packages.js +58 -0
  55. package/dist/release/candidates.d.ts +11 -0
  56. package/dist/release/candidates.d.ts.map +1 -0
  57. package/dist/release/candidates.js +181 -0
  58. package/dist/release/core.d.ts +4 -1
  59. package/dist/release/core.d.ts.map +1 -1
  60. package/dist/release/core.js +76 -9
  61. package/dist/release/github-release.d.ts +1 -0
  62. package/dist/release/github-release.d.ts.map +1 -1
  63. package/dist/release/github-release.js +3 -3
  64. package/dist/release/index.d.ts +32 -1
  65. package/dist/release/index.d.ts.map +1 -1
  66. package/dist/release/index.js +488 -64
  67. package/dist/release/npm-auth.d.ts +0 -1
  68. package/dist/release/npm-auth.d.ts.map +1 -1
  69. package/dist/release/npm-auth.js +4 -16
  70. package/dist/release/orchestration.d.ts +11 -2
  71. package/dist/release/orchestration.d.ts.map +1 -1
  72. package/dist/release/orchestration.js +41 -3
  73. package/dist/release/publish-plan.js +1 -1
  74. package/managed/raw/git-format-staged.yml +54 -0
  75. package/managed/raw/tooling/git-hooks/commit-msg.sh +1 -1
  76. package/managed/templates/github/actions/setup-devenv/action.yml +1 -7
  77. package/managed/templates/github/workflows/ci.yml +4 -2
  78. package/package.json +15 -8
  79. package/src/cli.ts +153 -22
  80. package/src/devenv/index.test.ts +49 -0
  81. package/src/devenv/index.ts +38 -0
  82. package/src/generate/index.ts +92 -0
  83. package/src/lib/cli-package.ts +18 -0
  84. package/src/lib/run.ts +44 -0
  85. package/src/lib/workspace.ts +112 -17
  86. package/src/monorepo/__tests__/publish-workflow.test.ts +16 -9
  87. package/src/monorepo/commit-msg.test.ts +45 -0
  88. package/src/monorepo/commit-msg.ts +107 -7
  89. package/src/monorepo/index.ts +142 -16
  90. package/src/monorepo/lockfile.ts +9 -0
  91. package/src/monorepo/managed-files.ts +1 -1
  92. package/src/monorepo/nx-sync.ts +9 -2
  93. package/src/monorepo/package-hygiene.test.ts +34 -0
  94. package/src/monorepo/package-hygiene.ts +26 -4
  95. package/src/monorepo/package-policy.test.ts +1130 -0
  96. package/src/monorepo/package-policy.ts +237 -114
  97. package/src/monorepo/packed-package.ts +89 -37
  98. package/src/monorepo/packs/index.test.ts +189 -0
  99. package/src/monorepo/packs/index.ts +311 -20
  100. package/src/monorepo/publish-workflow.ts +10 -19
  101. package/src/monorepo/setup-test-tracing.test.ts +120 -0
  102. package/src/monorepo/tool-validation.test.ts +261 -0
  103. package/src/monorepo/tool-validation.ts +462 -0
  104. package/src/nx/index.test.ts +73 -0
  105. package/src/nx/index.ts +139 -0
  106. package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
  107. package/src/release/__tests__/candidates.test.ts +262 -0
  108. package/src/release/__tests__/core-properties.test.ts +6 -6
  109. package/src/release/__tests__/core-scenarios.test.ts +27 -20
  110. package/src/release/__tests__/core.test.ts +52 -5
  111. package/src/release/__tests__/fixture-repo.test.ts +90 -31
  112. package/src/release/__tests__/github-release.test.ts +22 -24
  113. package/src/release/__tests__/helpers/fixture-repo.ts +1 -0
  114. package/src/release/__tests__/npm-auth.test.ts +12 -21
  115. package/src/release/__tests__/orchestration.test.ts +142 -12
  116. package/src/release/__tests__/publish-plan.test.ts +9 -3
  117. package/src/release/__tests__/retag-unpublished.test.ts +10 -10
  118. package/src/release/__tests__/trust-publisher.test.ts +165 -0
  119. package/src/release/bootstrap-npm-packages.ts +88 -0
  120. package/src/release/candidates.ts +225 -0
  121. package/src/release/core.ts +96 -10
  122. package/src/release/github-release.ts +3 -3
  123. package/src/release/index.ts +658 -74
  124. package/src/release/npm-auth.ts +4 -33
  125. package/src/release/orchestration.ts +60 -5
  126. package/src/release/publish-plan.ts +1 -1
  127. package/dist/nx-version-actions.cjs +0 -25
  128. package/dist/nx-version-actions.d.cts +0 -6
  129. package/dist/nx-version-actions.d.cts.map +0 -1
  130. package/managed/raw/.git-format-staged.yml +0 -47
  131. package/src/nx-version-actions.cts +0 -36
@@ -5,24 +5,26 @@ import { createInterface } from 'node:readline/promises';
5
5
  import { Writable } from 'node:stream';
6
6
  import { $ } from 'bun';
7
7
  import { withDevenvEnv } from '../lib/devenv.js';
8
- import { isRecord, stringProperty } from '../lib/json.js';
9
- import { decode, run, runStatus } from '../lib/run.js';
8
+ import { isRecord, readJsonObject, stringProperty } from '../lib/json.js';
9
+ import { decode, run, runInteractiveStatus, runResult, runStatus } from '../lib/run.js';
10
10
  import { listReleasePackages, readPackageJson, repositoryInfo } from '../lib/workspace.js';
11
11
  import { readPackedPackageJson, validatePackedWorkspaceDependencies } from '../monorepo/packed-manifest.js';
12
- import { collectOwnedReleaseTagRecords, pendingReleaseTargets, releaseTag, } from './core.js';
13
- import { createOrUpdateGithubRelease, renderNxProjectChangelogContents } from './github-release.js';
12
+ import { bootstrapNpmPackages, NPM_BOOTSTRAP_DIST_TAG, NPM_BOOTSTRAP_VERSION, } from './bootstrap-npm-packages.js';
13
+ import { autoReleaseCandidatePackages } from './candidates.js';
14
+ import { collectOwnedReleaseTagRecords, pendingReleaseTargets, releaseTag, releaseTagAliases, } from './core.js';
15
+ import { createOrUpdateGithubRelease, renderNxProjectChangelogContents, withNxWorkspaceRoot, } from './github-release.js';
14
16
  import { publishWithAuthDiagnostics } from './npm-auth.js';
15
- import { completeReleaseAtHead as completeReleaseAtHeadWithShell, repairPendingTargets, runReleaseVersion, } from './orchestration.js';
17
+ import { bumpStableReleaseToNext, completeReleaseAtHead as completeReleaseAtHeadWithShell, repairPendingTargets, runReleaseVersion, } from './orchestration.js';
16
18
  import { retagUnpublished } from './retag-unpublished.js';
17
19
  export async function releaseVersion(root, options) {
18
20
  const bump = releaseBumpArg(options.bump);
19
21
  const packages = releasePackages(root);
20
- const projects = releasePackageProjects(packages);
21
22
  const result = await runReleaseVersion({
22
23
  releasePackagesAtHead: () => releasePackagesAtHead(root, packages),
24
+ releaseVersionPackages: (releaseBump) => releaseVersionPackages(root, packages, releaseBump),
23
25
  ensureLocalReleaseTags: (releasePackages) => ensureLocalReleaseTags(root, releasePackages),
24
26
  gitHead: () => gitHead(root),
25
- runNxReleaseVersion: (releaseBump, dryRun) => runNxReleaseVersion(root, projects, releaseBump, dryRun),
27
+ runNxReleaseVersion: (releasePackages, releaseBump, dryRun) => runNxReleaseVersion(root, releasePackageProjects(releasePackages), releaseBump, dryRun),
26
28
  assertCleanGitTree: () => assertCleanGitTree(root),
27
29
  }, { bump, dryRun: options.dryRun === true });
28
30
  if (result.status === 'already-release-target') {
@@ -31,6 +33,9 @@ export async function releaseVersion(root, options) {
31
33
  else if (result.status === 'no-release-needed') {
32
34
  console.log('Nx did not create a release commit; no release needed.');
33
35
  }
36
+ if (result.status === 'dry-run') {
37
+ await writeReleasePreviewSummary(root, packages, result.packages);
38
+ }
34
39
  await writeReleaseGithubOutput(options.githubOutput, result.packages, result.mode);
35
40
  }
36
41
  export async function releasePublish(root, options) {
@@ -38,15 +43,20 @@ export async function releasePublish(root, options) {
38
43
  const packages = await releasePackagesAtHead(root, releasePackages(root));
39
44
  if (packages.length === 0) {
40
45
  if (bump === 'auto') {
46
+ if (options.dryRun === true) {
47
+ console.log('No release tags found at HEAD because dry-run versioning does not create release commits or tags.');
48
+ return;
49
+ }
41
50
  console.log('No release tags found at HEAD; no release to publish.');
42
51
  const summary = {
43
52
  sha: await gitHead(root),
44
- dryRun: options.dryRun === true,
53
+ dryRun: false,
45
54
  packages,
46
55
  pushed: false,
47
56
  published: [],
48
57
  alreadyPublished: [],
49
58
  githubReleases: [],
59
+ githubReleaseLinks: [],
50
60
  rerunRequired: false,
51
61
  noRelease: true,
52
62
  };
@@ -57,6 +67,10 @@ export async function releasePublish(root, options) {
57
67
  }
58
68
  const summary = await completeReleaseAtHeadWithShell(releaseCompletionShell(root), packages, options.dryRun === true, await newerCommitsRemain(root));
59
69
  await writeReleaseSummary(summary);
70
+ const bumpedPackages = await bumpStableReleaseToNext(releaseNextShell(root), packages, options.dryRun === true, summary.rerunRequired);
71
+ if (bumpedPackages.length > 0) {
72
+ console.log(`Prepared next prerelease for ${packageSummary(bumpedPackages)}.`);
73
+ }
60
74
  }
61
75
  export async function releaseRepairPending(root, options) {
62
76
  const branch = await releaseBranch(root);
@@ -70,61 +84,143 @@ export async function releaseRepairPending(root, options) {
70
84
  console.log(targets.length === 0
71
85
  ? 'Repair pending releases: no pending durable state repairs found.'
72
86
  : `Repair pending releases: ${targets.length} release target${targets.length === 1 ? '' : 's'} need repair.`);
87
+ for (const target of targets) {
88
+ console.log(`Repair pending releases: target ${target.sha.slice(0, 12)} needs ${repairTargetSummary(target)}.`);
89
+ }
73
90
  const summaries = await repairPendingTargets(releaseRepairShell(root), targets, restoreRef, options.dryRun === true);
74
91
  await writeRepairSummary(summaries, options.dryRun === true);
75
92
  }
76
93
  export async function releaseTrustPublisher(root, options) {
77
94
  const repository = githubRepositoryFromRootPackage(root);
78
95
  const workflow = 'publish.yml';
79
- const packages = listReleasePackages(root);
80
- if (packages.length === 0) {
96
+ await configureTrustedPublishers({
97
+ repository,
98
+ workflow,
99
+ listReleasePackages: () => listReleasePackages(root),
100
+ packageExists: (name) => npmPackageExists(root, name),
101
+ bootstrapNpmPackages: (bootstrapOptions) => bootstrapNpmPackages({
102
+ listReleasePackages: () => listReleasePackages(root),
103
+ packageExists: (name) => npmPackageExists(root, name),
104
+ login: () => runLatestNpm(root, ['login', '--auth-type=web']),
105
+ publishPlaceholder: (pkg, env) => publishPlaceholderPackage(root, pkg, env),
106
+ promptOtp: (packageName) => promptForNpmOtp(packageName),
107
+ log: (message) => console.log(message),
108
+ }, bootstrapOptions),
109
+ trustPublisher: (pkg, dryRun, env) => {
110
+ const args = ['trust', 'github', pkg.name, '--file', workflow, '--repo', repository, '--yes'];
111
+ if (dryRun) {
112
+ args.push('--dry-run');
113
+ }
114
+ return runLatestNpmTrust(root, args, env);
115
+ },
116
+ trustedPublishers: (pkg) => listTrustedPublishers(root, pkg),
117
+ log: (message) => console.log(message),
118
+ error: (message) => console.error(message),
119
+ }, options);
120
+ }
121
+ export async function configureTrustedPublishers(shell, options) {
122
+ const packages = shell.listReleasePackages();
123
+ const selectedPackages = selectedTrustPublisherPackages(packages, options.packages ?? []);
124
+ if (selectedPackages.length === 0) {
81
125
  throw new Error('No owned release packages found.');
82
126
  }
127
+ if (options.bootstrap) {
128
+ await shell.bootstrapNpmPackages({
129
+ dryRun: options.dryRun === true,
130
+ skipLogin: options.skipLogin === true,
131
+ otp: options.bootstrapOtp,
132
+ packages: options.packages ?? [],
133
+ });
134
+ }
83
135
  if (!options.dryRun) {
84
- const packageStates = await Promise.all(packages.map(async (pkg) => ((await npmPackageExists(root, pkg.name)) ? null : pkg.name)));
136
+ const packageStates = await Promise.all(selectedPackages.map(async (pkg) => ((await shell.packageExists(pkg.name)) ? null : pkg.name)));
85
137
  const missingPackages = packageStates.filter((name) => name !== null);
86
138
  if (missingPackages.length > 0) {
87
139
  throw new Error('npm trusted publishing can only be configured after packages exist on the registry. ' +
88
- 'Bootstrap the first publish with a temporary NPM_TOKEN, then rerun trust-publisher. ' +
140
+ 'Run smoo release trust-publisher --bootstrap locally. ' +
89
141
  `Missing packages: ${missingPackages.join(', ')}`);
90
142
  }
91
143
  }
92
- if (!options.dryRun && !options.skipLogin) {
93
- await runLatestNpm(root, ['login', '--auth-type=web']);
94
- }
95
144
  const failedPackages = [];
96
- for (const pkg of packages) {
97
- console.log(`${pkg.name}: trusting GitHub Actions ${repository}/${workflow}`);
98
- const args = ['trust', 'github', pkg.name, '--file', workflow, '--repo', repository, '--yes'];
99
- if (options.dryRun) {
100
- args.push('--dry-run');
145
+ for (const pkg of selectedPackages) {
146
+ if (!options.dryRun) {
147
+ const trustedPublishers = await shell.trustedPublishers(pkg);
148
+ if (hasMatchingGithubTrustedPublisher(trustedPublishers, shell.repository, shell.workflow)) {
149
+ shell.log(`${pkg.name}: npm trusted publisher is already configured; skipping.`);
150
+ continue;
151
+ }
152
+ if (trustedPublishers.length > 0) {
153
+ throw new Error(`${pkg.name}: npm trusted publisher exists but does not match GitHub Actions ${shell.repository}/${shell.workflow}. ` +
154
+ 'Use npm trust list to inspect it and npm trust revoke --id <trust-id> before reconfiguring.');
155
+ }
101
156
  }
157
+ shell.log(`${pkg.name}: trusting GitHub Actions ${shell.repository}/${shell.workflow}`);
102
158
  while (true) {
103
- const otp = options.dryRun ? undefined : (options.otp ?? (await promptForNpmOtp(pkg.name)));
104
159
  try {
105
- await runLatestNpm(root, args, otp ? { NPM_CONFIG_OTP: otp } : undefined);
160
+ const result = await shell.trustPublisher(pkg, options.dryRun === true);
161
+ if (result === 'already-configured') {
162
+ shell.log(`${pkg.name}: npm trusted publisher is already configured; skipping.`);
163
+ }
106
164
  break;
107
165
  }
108
166
  catch (error) {
109
- console.error(`${pkg.name}: npm trusted publisher setup failed.`);
110
- console.error(error instanceof Error ? error.message : String(error));
167
+ shell.error(`${pkg.name}: npm trusted publisher setup failed.`);
168
+ shell.error(error instanceof Error ? error.message : String(error));
111
169
  if (!options.dryRun) {
112
- console.error('npm OTP codes are single-use. Generate a fresh OTP before retrying this package.');
170
+ shell.error('npm owns trusted publishing authentication. Complete the npm browser challenge, then retry.');
113
171
  }
114
- if (options.otp || options.dryRun) {
172
+ if (options.dryRun) {
115
173
  failedPackages.push(pkg.name);
116
174
  break;
117
175
  }
118
- console.error('Retrying this package. Press Ctrl-C to stop.');
176
+ shell.error('Retrying this package. Press Ctrl-C to stop.');
119
177
  }
120
178
  }
121
179
  }
122
180
  if (failedPackages.length > 0) {
123
- console.error(`Trusted publishing was not configured for: ${failedPackages.join(', ')}`);
124
- if (options.otp) {
125
- console.error('Rerun smoo release trust-publisher without --otp to enter a fresh OTP for each failed package.');
181
+ shell.error(`Trusted publishing was not configured for: ${failedPackages.join(', ')}`);
182
+ }
183
+ }
184
+ function selectedTrustPublisherPackages(packages, selections) {
185
+ if (selections.length === 0) {
186
+ return packages;
187
+ }
188
+ const byName = new Map(packages.map((pkg) => [pkg.name, pkg]));
189
+ const selected = [];
190
+ const unknown = [];
191
+ for (const name of selections) {
192
+ const pkg = byName.get(name);
193
+ if (pkg) {
194
+ selected.push(pkg);
126
195
  }
196
+ else {
197
+ unknown.push(name);
198
+ }
199
+ }
200
+ if (unknown.length > 0) {
201
+ throw new Error(`Unknown owned release package selection: ${unknown.join(', ')}`);
127
202
  }
203
+ return selected;
204
+ }
205
+ function hasMatchingGithubTrustedPublisher(trustedPublishers, repository, workflow) {
206
+ return trustedPublishers.some((trustedPublisher) => trustedPublisher.type === 'github' &&
207
+ trustedPublisher.repository === repository &&
208
+ trustedPublisher.file === workflow);
209
+ }
210
+ export async function releaseBootstrapNpmPackages(root, options) {
211
+ await bootstrapNpmPackages({
212
+ listReleasePackages: () => listReleasePackages(root),
213
+ packageExists: (name) => npmPackageExists(root, name),
214
+ login: () => runLatestNpm(root, ['login', '--auth-type=web']),
215
+ publishPlaceholder: (pkg, env) => publishPlaceholderPackage(root, pkg, env),
216
+ promptOtp: (packageName) => promptForNpmOtp(packageName),
217
+ log: (message) => console.log(message),
218
+ }, {
219
+ dryRun: options.dryRun === true,
220
+ skipLogin: options.skipLogin === true,
221
+ otp: options.otp,
222
+ packages: options.packages ?? [],
223
+ });
128
224
  }
129
225
  export async function releaseRetagUnpublished(root, options) {
130
226
  const toRef = options.to ?? 'HEAD';
@@ -145,6 +241,7 @@ export async function releaseRetagUnpublished(root, options) {
145
241
  export async function printReleaseState(root) {
146
242
  console.log(JSON.stringify(await getReleaseState(root), null, 2));
147
243
  }
244
+ const releasePreviewChangelogs = new Map();
148
245
  async function getReleaseState(root) {
149
246
  return getReleaseStateForPackages(root, listReleasePackages(root));
150
247
  }
@@ -159,15 +256,14 @@ async function listUnpublishedPackages(root, packages) {
159
256
  const states = await Promise.all(packages.map(async (pkg) => ({ pkg, published: await npmVersionExists(root, pkg.name, pkg.version) })));
160
257
  return states.filter((state) => !state.published).map((state) => state.pkg);
161
258
  }
162
- async function publishPackedPackage(root, pkg, tag, dryRun, useBootstrapToken) {
259
+ async function publishPackedPackage(root, pkg, tag, dryRun) {
163
260
  const tempDir = await mkdtemp(join(tmpdir(), 'smoo-publish-'));
164
261
  const tarball = join(tempDir, `${safeTarballPrefix(pkg.name)}-${pkg.version}.tgz`);
165
262
  try {
166
263
  console.log(`${pkg.name}@${pkg.version}: packing with bun pm pack`);
167
264
  await run('bun', ['pm', 'pack', '--filename', tarball, '--ignore-scripts', '--quiet'], join(root, pkg.path));
168
265
  await assertPackedWorkspaceDependencies(root, tarball, pkg);
169
- // npm CLI owns authentication here: trusted publishing OIDC when configured,
170
- // or the temporary NODE_AUTH_TOKEN bootstrap path below before trust exists.
266
+ // npm CLI owns authentication here: trusted publishing OIDC when configured.
171
267
  // Bun still produces the tarball so workspace:* dependencies are resolved the
172
268
  // same way smoo validates packed packages before release.
173
269
  const args = ['publish', tarball, '--access', 'public', '--tag', tag, '--provenance'];
@@ -175,7 +271,7 @@ async function publishPackedPackage(root, pkg, tag, dryRun, useBootstrapToken) {
175
271
  args.push('--dry-run');
176
272
  }
177
273
  await publishWithAuthDiagnostics(pkg, {
178
- publish: () => runLatestNpmPublish(root, args, useBootstrapToken),
274
+ publish: () => runLatestNpmPublish(root, args),
179
275
  versionExists: () => npmVersionExists(root, pkg.name, pkg.version),
180
276
  log: (message) => console.log(message),
181
277
  error: (message) => console.error(message),
@@ -186,7 +282,6 @@ async function publishPackedPackage(root, pkg, tag, dryRun, useBootstrapToken) {
186
282
  }
187
283
  },
188
284
  }, {
189
- useBootstrapToken,
190
285
  tokenPresent: npmAuthTokenPresent(),
191
286
  repository: githubRepositoryFromRootPackage(root),
192
287
  });
@@ -198,6 +293,24 @@ async function publishPackedPackage(root, pkg, tag, dryRun, useBootstrapToken) {
198
293
  function npmAuthTokenPresent() {
199
294
  return Boolean(process.env.NODE_AUTH_TOKEN || process.env.NPM_TOKEN || process.env.NPM_CONFIG_USERCONFIG);
200
295
  }
296
+ async function publishPlaceholderPackage(root, pkg, env) {
297
+ const tempDir = await mkdtemp(join(tmpdir(), 'smoo-npm-bootstrap-'));
298
+ try {
299
+ await writeFile(join(tempDir, 'package.json'), `${JSON.stringify({
300
+ name: pkg.name,
301
+ version: NPM_BOOTSTRAP_VERSION,
302
+ description: `Bootstrap placeholder for ${pkg.name}. Real releases are published by SmoothBricks CI.`,
303
+ license: stringProperty(pkg.json, 'license') ?? 'UNLICENSED',
304
+ repository: pkg.json.repository,
305
+ publishConfig: { access: 'public' },
306
+ }, null, 2)}\n`);
307
+ await writeFile(join(tempDir, 'README.md'), `# ${pkg.name}\n\nThis is a bootstrap placeholder. Real package versions are published by SmoothBricks release automation.\n`);
308
+ await runLatestNpm(root, ['publish', tempDir, '--access', 'public', '--tag', NPM_BOOTSTRAP_DIST_TAG], env);
309
+ }
310
+ finally {
311
+ await rm(tempDir, { recursive: true, force: true });
312
+ }
313
+ }
201
314
  function safeTarballPrefix(name) {
202
315
  return name.replace(/^@/, '').replace(/[^a-zA-Z0-9._-]+/g, '-');
203
316
  }
@@ -216,9 +329,25 @@ function releasePackages(root) {
216
329
  return packages;
217
330
  }
218
331
  function releasePackageProjects(packages) {
219
- return packages.map((pkg) => pkg.name).join(',');
332
+ return packages.map((pkg) => pkg.projectName).join(',');
333
+ }
334
+ async function releaseVersionPackages(root, packages, bump) {
335
+ if (bump !== 'auto') {
336
+ return packages;
337
+ }
338
+ return autoReleaseCandidatePackages({
339
+ gitRefExists: (ref) => gitRefExists(root, ref),
340
+ packageChangedFilesSince: (ref, packagePath) => packageChangedFilesSince(root, ref, packagePath),
341
+ packageJsonAtRef: (ref, packagePath) => packageJsonAtRef(root, ref, packagePath),
342
+ currentPackageJson: (packagePath) => currentPackageJson(root, packagePath),
343
+ packageBuildInputPatterns: (projectName, packagePath) => packageBuildInputPatterns(root, projectName, packagePath),
344
+ packageHasHistory: (packagePath) => packageHasHistory(root, packagePath),
345
+ }, packages);
220
346
  }
221
347
  async function runNxReleaseVersion(root, projects, bump, dryRun) {
348
+ if (dryRun) {
349
+ return runNxReleaseVersionPreview(root, projects, bump);
350
+ }
222
351
  // Nx owns local release mutation: package versions, bun.lock updates, the
223
352
  // release commit, and annotated tags. smoo owns remote publication after the
224
353
  // workflow validates the exact release commit Nx produced.
@@ -227,10 +356,66 @@ async function runNxReleaseVersion(root, projects, bump, dryRun) {
227
356
  nxArgs.push(bump);
228
357
  }
229
358
  nxArgs.push(`--projects=${projects}`, '--git-commit=true', '--git-tag=true', '--git-push=false');
230
- if (dryRun) {
231
- nxArgs.push('--dry-run');
232
- }
233
359
  await run('nx', nxArgs, root);
360
+ return [];
361
+ }
362
+ async function runNxReleaseVersionPreview(root, projects, bump) {
363
+ return withNxWorkspaceRoot(root, async () => {
364
+ const { createAPI } = await import('nx/src/command-line/release/version.js');
365
+ const { createAPI: createChangelogAPI } = await import('nx/src/command-line/release/changelog.js');
366
+ const projectNames = projects.split(',').filter(Boolean);
367
+ const result = await createAPI({}, false)({
368
+ specifier: bump === 'auto' ? undefined : bump,
369
+ projects: projectNames,
370
+ gitCommit: true,
371
+ gitTag: true,
372
+ gitPush: false,
373
+ dryRun: true,
374
+ });
375
+ const changelogResult = await createChangelogAPI({
376
+ changelog: {
377
+ workspaceChangelog: false,
378
+ projectChangelogs: { createRelease: false, file: false },
379
+ },
380
+ }, false)({
381
+ projects: projectNames,
382
+ versionData: result.projectsVersionData,
383
+ releaseGraph: result.releaseGraph,
384
+ gitCommit: false,
385
+ gitTag: false,
386
+ gitPush: false,
387
+ stageChanges: false,
388
+ createRelease: false,
389
+ dryRun: true,
390
+ });
391
+ releasePreviewChangelogs.clear();
392
+ for (const [projectName, changelog] of Object.entries(changelogResult.projectChangelogs ?? {})) {
393
+ releasePreviewChangelogs.set(projectName, changelog.contents);
394
+ }
395
+ const packagesByProject = new Map(releasePackages(root).map((pkg) => [pkg.projectName, pkg]));
396
+ return Object.entries(result.projectsVersionData)
397
+ .map(([projectName, data]) => {
398
+ const pkg = packagesByProject.get(projectName);
399
+ if (!pkg || !data.newVersion) {
400
+ return null;
401
+ }
402
+ return { ...pkg, version: data.newVersion };
403
+ })
404
+ .filter((pkg) => pkg !== null);
405
+ });
406
+ }
407
+ async function runNxNextPrereleaseVersion(root, projects) {
408
+ await run('nx', [
409
+ 'release',
410
+ 'version',
411
+ 'prerelease',
412
+ `--projects=${projects}`,
413
+ '--preid=next',
414
+ '--git-commit=true',
415
+ '--git-tag=false',
416
+ '--git-push=false',
417
+ '--git-commit-message=chore(release): prepare next prerelease',
418
+ ], root);
234
419
  }
235
420
  async function releasePackagesAtHead(root, packages) {
236
421
  return releasePackagesAtRef(root, packages, 'HEAD');
@@ -299,7 +484,7 @@ async function gitTagsAtRef(root, ref) {
299
484
  .filter(Boolean);
300
485
  }
301
486
  async function previousReleaseTag(root, pkg, currentTag) {
302
- const result = await $ `git tag --list ${`${pkg.name}@*`} --sort=-v:refname --merged ${currentTag}`
487
+ const result = await $ `git tag --list ${`${pkg.projectName}@*`} --sort=-v:refname --merged ${currentTag}`
303
488
  .cwd(root)
304
489
  .quiet()
305
490
  .nothrow();
@@ -347,6 +532,11 @@ async function pushReleaseRefs(root, packages) {
347
532
  await run('git', ['push', '--atomic', remote, ...refspecs], root);
348
533
  return true;
349
534
  }
535
+ async function pushCurrentBranch(root) {
536
+ const branch = await releaseBranch(root);
537
+ const remote = await releaseRemote(root, branch);
538
+ await run('git', ['push', remote, `HEAD:refs/heads/${branch}`], root);
539
+ }
350
540
  function releaseCompletionShell(root) {
351
541
  return {
352
542
  gitHead: () => gitHead(root),
@@ -355,12 +545,23 @@ function releaseCompletionShell(root) {
355
545
  buildReleaseCandidate: (packages) => buildReleaseCandidate(root, packages),
356
546
  publishPackage: async (pkg, distTag, dryRun) => {
357
547
  const packageExists = dryRun ? true : await npmPackageExists(root, pkg.name);
358
- await publishPackedPackage(root, pkg, distTag, dryRun, !dryRun && !packageExists);
548
+ if (!packageExists) {
549
+ throw new Error(missingNpmPackagePublishGuidance(pkg));
550
+ }
551
+ await publishPackedPackage(root, pkg, distTag, dryRun);
359
552
  },
360
553
  listGithubMissingPackages: (packages) => listMissingGithubReleasePackages(root, packages),
361
554
  createGithubRelease: (pkg, dryRun) => createGithubRelease(root, pkg, dryRun),
362
555
  };
363
556
  }
557
+ function releaseNextShell(root) {
558
+ return {
559
+ bumpStablePackagesToNext: async (packages) => {
560
+ await runNxNextPrereleaseVersion(root, releasePackageProjects(packages));
561
+ await pushCurrentBranch(root);
562
+ },
563
+ };
564
+ }
364
565
  function releaseRepairShell(root) {
365
566
  return {
366
567
  ...releaseCompletionShell(root),
@@ -405,15 +606,23 @@ async function listOwnedReleaseTagRecords(root, ref) {
405
606
  }
406
607
  async function durableReleaseTagState(root, pkg, tag) {
407
608
  const packageVersion = `${pkg.name}@${pkg.version}`;
609
+ const githubReleaseTags = releaseTagAliases({ ...pkg, projectName: projectNameFromReleaseTag(tag) });
408
610
  const start = Date.now();
409
- console.log(`${packageVersion}: checking durable state (npm + GitHub Release ${tag}).`);
611
+ console.log(`${packageVersion}: checking durable state (npm + GitHub Release ${githubReleaseTags.join(' or ')}).`);
410
612
  const [npmPublished, githubReleasePresent] = await Promise.all([
411
613
  npmVersionExists(root, pkg.name, pkg.version),
412
- githubReleaseExists(root, tag),
614
+ anyGithubReleaseExists(root, githubReleaseTags),
413
615
  ]);
414
616
  console.log(`${packageVersion}: durable state npm=${yesNo(npmPublished)} github=${yesNo(githubReleasePresent)} (${Date.now() - start}ms).`);
415
617
  return { npmPublished, githubReleaseExists: githubReleasePresent };
416
618
  }
619
+ function projectNameFromReleaseTag(tag) {
620
+ const versionSeparator = tag.lastIndexOf('@');
621
+ if (versionSeparator <= 0) {
622
+ return tag;
623
+ }
624
+ return tag.slice(0, versionSeparator);
625
+ }
417
626
  async function gitReleaseTagsByCreatorDate(root) {
418
627
  const result = await $ `git for-each-ref --sort=-creatordate --format=${'%(refname:short)%09%(creatordate:unix)%09%(*objectname)%09%(objectname)'} refs/tags`
419
628
  .cwd(root)
@@ -452,7 +661,7 @@ async function createGithubRelease(root, pkg, dryRun) {
452
661
  const previousTag = await previousReleaseTag(root, pkg, currentTag);
453
662
  if (dryRun) {
454
663
  await renderNxProjectChangelogContents({ root, pkg, previousTag, dryRun });
455
- return;
664
+ return null;
456
665
  }
457
666
  const contents = await renderNxProjectChangelogContents({ root, pkg, previousTag, dryRun });
458
667
  await createOrUpdateGithubRelease(pkg, contents, {
@@ -460,6 +669,7 @@ async function createGithubRelease(root, pkg, dryRun) {
460
669
  runGhRelease: (args) => run('gh', args, root),
461
670
  log: (message) => console.log(message),
462
671
  });
672
+ return githubReleaseUrl(root, currentTag);
463
673
  }
464
674
  function envPresence(name) {
465
675
  return `${name}=${process.env[name] ? 'present' : 'missing'}`;
@@ -494,7 +704,7 @@ async function writeReleaseSummary(summary) {
494
704
  lines.push(`- Git refs pushed: ${summary.pushed ? 'yes' : 'already current'}`);
495
705
  lines.push(`- npm published: ${packageSummary(summary.published)}`);
496
706
  lines.push(`- npm already published: ${packageSummary(summary.alreadyPublished)}`);
497
- lines.push(`- GitHub Releases created/updated: ${packageSummary(summary.githubReleases)}`);
707
+ lines.push(`- GitHub Releases created/updated: ${githubReleaseSummary(summary)}`);
498
708
  }
499
709
  if (summary.rerunRequired) {
500
710
  const message = 'A previous incomplete release was repaired; newer commits remain. Run Publish again.';
@@ -508,6 +718,32 @@ async function writeReleaseSummary(summary) {
508
718
  await appendFile(summaryPath, `${text}\n`);
509
719
  }
510
720
  }
721
+ async function writeReleasePreviewSummary(root, currentPackages, previewPackages) {
722
+ const lines = ['## Release Preview', '', `- Commit: \`${(await gitHead(root)).slice(0, 12)}\``, '- Mode: dry run'];
723
+ if (previewPackages.length === 0) {
724
+ lines.push('- Result: no release needed');
725
+ }
726
+ else {
727
+ lines.push('- Packages:');
728
+ const currentByName = new Map(currentPackages.map((pkg) => [pkg.name, pkg]));
729
+ for (const pkg of previewPackages) {
730
+ lines.push(` - \`${pkg.name}\`: \`${currentByName.get(pkg.name)?.version ?? 'unknown'}\` -> \`${pkg.version}\``);
731
+ }
732
+ for (const pkg of previewPackages) {
733
+ const contents = releasePreviewChangelogs.get(pkg.projectName);
734
+ if (!contents) {
735
+ throw new Error(`Nx did not generate a project changelog for ${pkg.projectName}.`);
736
+ }
737
+ lines.push('', `### ${pkg.name} ${pkg.version}`, '', contents.trim());
738
+ }
739
+ }
740
+ const text = `${lines.join('\n')}\n`;
741
+ console.log(text.trimEnd());
742
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
743
+ if (summaryPath) {
744
+ await appendFile(summaryPath, `${text}\n`);
745
+ }
746
+ }
511
747
  async function writeRepairSummary(summaries, dryRun) {
512
748
  const lines = ['## Pending Release Repair', '', `- Mode: ${dryRun ? 'dry run' : 'release'}`];
513
749
  if (summaries.length === 0) {
@@ -515,7 +751,7 @@ async function writeRepairSummary(summaries, dryRun) {
515
751
  }
516
752
  else {
517
753
  for (const summary of summaries) {
518
- lines.push(`- Repaired \`${summary.sha.slice(0, 12)}\`: ${packageSummary(summary.packages)}`);
754
+ lines.push(`- ${dryRun ? 'Would repair' : 'Repaired'} \`${summary.sha.slice(0, 12)}\`: ${packageSummary(summary.packages)}`);
519
755
  }
520
756
  }
521
757
  const text = `${lines.join('\n')}\n`;
@@ -531,6 +767,27 @@ function packageSummary(packages) {
531
767
  }
532
768
  return packages.map((pkg) => `${pkg.name}@${pkg.version}`).join(', ');
533
769
  }
770
+ function githubReleaseSummary(summary) {
771
+ if (summary.githubReleaseLinks.length > 0) {
772
+ return summary.githubReleaseLinks.map(({ pkg, url }) => `[${pkg.name}@${pkg.version}](${url})`).join(', ');
773
+ }
774
+ return packageSummary(summary.githubReleases);
775
+ }
776
+ function repairTargetSummary(target) {
777
+ return target.packages
778
+ .map((pkg) => `${pkg.name}@${pkg.version} (${repairReasons(target, pkg).join(' + ')})`)
779
+ .join(', ');
780
+ }
781
+ function repairReasons(target, pkg) {
782
+ const reasons = [];
783
+ if (target.npmPackages.includes(pkg)) {
784
+ reasons.push('npm missing');
785
+ }
786
+ if (target.githubPackages.includes(pkg)) {
787
+ reasons.push('GitHub Release missing');
788
+ }
789
+ return reasons;
790
+ }
534
791
  async function fetchReleaseRefs(root, remote, branch) {
535
792
  await run('git', ['fetch', '--tags', remote, branch], root);
536
793
  }
@@ -538,6 +795,125 @@ async function gitRefExists(root, ref) {
538
795
  const result = await $ `git rev-parse --verify ${ref}`.cwd(root).quiet().nothrow();
539
796
  return result.exitCode === 0;
540
797
  }
798
+ async function packageChangedFilesSince(root, ref, packagePath) {
799
+ const result = await $ `git diff --name-only ${`${ref}..HEAD`} -- ${packagePath}`.cwd(root).quiet().nothrow();
800
+ if (result.exitCode !== 0) {
801
+ throw new Error(`Unable to inspect package changes under ${packagePath}.`);
802
+ }
803
+ const packagePrefix = `${packagePath}/`;
804
+ return decode(result.stdout)
805
+ .split('\n')
806
+ .map((path) => path.trim())
807
+ .filter(Boolean)
808
+ .map((path) => (path.startsWith(packagePrefix) ? path.slice(packagePrefix.length) : path));
809
+ }
810
+ async function packageJsonAtRef(root, ref, packagePath) {
811
+ const result = await $ `git show ${`${ref}:${packagePath}/package.json`}`.cwd(root).quiet().nothrow();
812
+ if (result.exitCode !== 0) {
813
+ return null;
814
+ }
815
+ try {
816
+ const parsed = JSON.parse(decode(result.stdout));
817
+ return isRecord(parsed) ? parsed : null;
818
+ }
819
+ catch {
820
+ return null;
821
+ }
822
+ }
823
+ async function currentPackageJson(root, packagePath) {
824
+ return readPackageJson(join(root, packagePath, 'package.json'))?.json ?? null;
825
+ }
826
+ async function packageBuildInputPatterns(root, projectName, _packagePath) {
827
+ const project = await nxProjectJson(root, projectName);
828
+ const nxJson = readJsonObject(join(root, 'nx.json')) ?? {};
829
+ return resolveBuildInputPatterns(project, nxJson);
830
+ }
831
+ async function nxProjectJson(root, projectName) {
832
+ const result = await $ `nx show project ${projectName} --json`.cwd(root).quiet();
833
+ const parsed = JSON.parse(decode(result.stdout));
834
+ if (!isRecord(parsed)) {
835
+ throw new Error(`Unable to inspect Nx project ${projectName}.`);
836
+ }
837
+ return parsed;
838
+ }
839
+ function resolveBuildInputPatterns(project, nxJson) {
840
+ const targets = recordProperty(project, 'targets');
841
+ if (!targets) {
842
+ return [];
843
+ }
844
+ return normalizeInputPatterns(collectBuildInputs(targets), nxJson);
845
+ }
846
+ function collectBuildInputs(targets) {
847
+ const build = recordProperty(targets, 'build');
848
+ if (!build) {
849
+ return ['production'];
850
+ }
851
+ const directInputs = stringArrayProperty(build, 'inputs');
852
+ if (directInputs.length > 0) {
853
+ return directInputs;
854
+ }
855
+ const inputs = [];
856
+ for (const dependency of stringArrayProperty(build, 'dependsOn')) {
857
+ if (dependency.startsWith('^')) {
858
+ continue;
859
+ }
860
+ const targetName = dependency.includes(':') ? dependency.split(':')[1] : dependency;
861
+ if (!targetName) {
862
+ continue;
863
+ }
864
+ inputs.push(...stringArrayProperty(recordProperty(targets, targetName), 'inputs'));
865
+ }
866
+ return inputs.length > 0 ? inputs : ['production'];
867
+ }
868
+ function normalizeInputPatterns(inputs, nxJson) {
869
+ const patterns = [];
870
+ const seen = new Set();
871
+ for (const input of inputs) {
872
+ for (const pattern of expandInputPattern(input, nxJson, seen)) {
873
+ patterns.push(pattern);
874
+ }
875
+ }
876
+ return patterns;
877
+ }
878
+ function expandInputPattern(input, nxJson, seen) {
879
+ if (seen.has(input)) {
880
+ return [];
881
+ }
882
+ seen.add(input);
883
+ if (!input.includes('{')) {
884
+ const namedInputs = recordProperty(nxJson, 'namedInputs');
885
+ const namedInput = namedInputs?.[input];
886
+ if (Array.isArray(namedInput)) {
887
+ return namedInput.flatMap((entry) => (typeof entry === 'string' ? expandInputPattern(entry, nxJson, seen) : []));
888
+ }
889
+ return [];
890
+ }
891
+ const excluded = input.startsWith('!');
892
+ const rawInput = excluded ? input.slice(1) : input;
893
+ if (!rawInput.startsWith('{projectRoot}/')) {
894
+ return [];
895
+ }
896
+ const packageRelative = rawInput.slice('{projectRoot}/'.length);
897
+ return [`${excluded ? '!' : ''}${packageRelative}`];
898
+ }
899
+ function recordProperty(record, key) {
900
+ if (!record) {
901
+ return null;
902
+ }
903
+ const value = record[key];
904
+ return isRecord(value) ? value : null;
905
+ }
906
+ function stringArrayProperty(record, key) {
907
+ const value = record?.[key];
908
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
909
+ }
910
+ async function packageHasHistory(root, packagePath) {
911
+ const result = await $ `git log --format=%H -- ${packagePath}`.cwd(root).quiet().nothrow();
912
+ if (result.exitCode !== 0) {
913
+ throw new Error(`Unable to inspect package history under ${packagePath}.`);
914
+ }
915
+ return decode(result.stdout).trim().length > 0;
916
+ }
541
917
  async function gitIsAncestor(root, ancestor, descendant) {
542
918
  const result = await $ `git merge-base --is-ancestor ${ancestor} ${descendant}`.cwd(root).quiet().nothrow();
543
919
  return result.exitCode === 0;
@@ -602,6 +978,17 @@ async function pushRetaggedReleaseTags(root, remote, updates) {
602
978
  async function githubReleaseExists(root, tag) {
603
979
  return (await runStatus('gh', ['release', 'view', tag, '--json', 'tagName'], root, true)) === 0;
604
980
  }
981
+ function githubReleaseUrl(root, tag) {
982
+ return `https://github.com/${githubRepositoryFromRootPackage(root)}/releases/tag/${encodeURIComponent(tag)}`;
983
+ }
984
+ async function anyGithubReleaseExists(root, tags) {
985
+ for (const tag of tags) {
986
+ if (await githubReleaseExists(root, tag)) {
987
+ return true;
988
+ }
989
+ }
990
+ return false;
991
+ }
605
992
  async function assertRemoteTagExists(root, tag) {
606
993
  const branch = await releaseBranch(root);
607
994
  const remote = await releaseRemote(root, branch);
@@ -712,32 +1099,69 @@ function githubRepositoryFromUrl(url) {
712
1099
  async function runLatestNpm(root, npmArgs, env) {
713
1100
  await run('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...npmArgs], root, env);
714
1101
  }
715
- async function runLatestNpmPublish(root, npmArgs, useBootstrapToken) {
716
- if (!useBootstrapToken) {
717
- await runLatestNpm(root, npmArgs, { NODE_AUTH_TOKEN: '', NPM_TOKEN: '' });
718
- return;
1102
+ async function runLatestNpmTrust(root, npmArgs, env) {
1103
+ const status = await runInteractiveStatus('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...npmArgs], root, env);
1104
+ if (status === 0) {
1105
+ return 'configured';
719
1106
  }
720
- const token = process.env.NODE_AUTH_TOKEN || process.env.NPM_TOKEN;
721
- if (!token || process.env.NPM_CONFIG_USERCONFIG) {
722
- if (!token && !process.env.NPM_CONFIG_USERCONFIG) {
723
- throw new Error('First publish for a package requires NODE_AUTH_TOKEN or NPM_TOKEN until trusted publishing exists.');
1107
+ throw new Error(`nix shell nixpkgs#nodejs_latest -c npm ${npmArgs.join(' ')} failed with exit code ${status}`);
1108
+ }
1109
+ async function listTrustedPublishers(root, pkg) {
1110
+ const args = ['trust', 'list', pkg.name, '--json'];
1111
+ let result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
1112
+ if (result.exitCode !== 0 && /\bEOTP\b/.test(`${result.stdout}\n${result.stderr}`)) {
1113
+ const status = await runInteractiveStatus('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
1114
+ if (status !== 0) {
1115
+ throw new Error(`nix shell nixpkgs#nodejs_latest -c npm ${args.join(' ')} failed with exit code ${status}`);
724
1116
  }
725
- await runLatestNpm(root, npmArgs);
726
- return;
1117
+ result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
727
1118
  }
728
- const tempDir = await mkdtemp(join(tmpdir(), 'smoo-npm-auth-'));
729
- const npmrc = join(tempDir, '.npmrc');
730
- try {
731
- await writeFile(npmrc, `registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=${token}\n`);
732
- await runLatestNpm(root, npmArgs, { NPM_CONFIG_USERCONFIG: npmrc });
1119
+ if (result.exitCode !== 0) {
1120
+ throw new Error(`nix shell nixpkgs#nodejs_latest -c npm ${args.join(' ')} failed with exit code ${result.exitCode}`);
733
1121
  }
734
- finally {
735
- await rm(tempDir, { recursive: true, force: true });
1122
+ return parseTrustedPublishers(result.stdout, pkg.name);
1123
+ }
1124
+ export function parseTrustedPublishers(stdout, packageName) {
1125
+ if (stdout.trim().length === 0) {
1126
+ return [];
1127
+ }
1128
+ const parsed = JSON.parse(stdout);
1129
+ if (parsed === null) {
1130
+ return [];
736
1131
  }
1132
+ if (Array.isArray(parsed)) {
1133
+ return parsed.map((value) => parseTrustedPublisher(value, packageName));
1134
+ }
1135
+ if (isRecord(parsed)) {
1136
+ return [parseTrustedPublisher(parsed, packageName)];
1137
+ }
1138
+ throw new Error(`${packageName}: npm trust list returned invalid JSON.`);
1139
+ }
1140
+ function parseTrustedPublisher(value, packageName) {
1141
+ if (!isRecord(value)) {
1142
+ throw new Error(`${packageName}: npm trust list returned invalid trusted publisher entry.`);
1143
+ }
1144
+ const id = stringProperty(value, 'id');
1145
+ const type = stringProperty(value, 'type');
1146
+ if (!id || !type) {
1147
+ throw new Error(`${packageName}: npm trust list returned a trusted publisher without id or type.`);
1148
+ }
1149
+ return {
1150
+ id,
1151
+ type,
1152
+ file: stringProperty(value, 'file') ?? undefined,
1153
+ repository: stringProperty(value, 'repository') ?? undefined,
1154
+ };
1155
+ }
1156
+ async function runLatestNpmPublish(root, npmArgs) {
1157
+ await runLatestNpm(root, npmArgs, { NODE_AUTH_TOKEN: '', NPM_TOKEN: '' });
1158
+ }
1159
+ function missingNpmPackagePublishGuidance(pkg) {
1160
+ return `${pkg.name} does not exist on npm yet. Run smoo release trust-publisher --bootstrap locally before rerunning the Publish workflow.`;
737
1161
  }
738
1162
  async function promptForNpmOtp(packageName) {
739
1163
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
740
- throw new Error(`npm trust requires a one-time password for ${packageName}. Pass --otp <code> in non-interactive shells.`);
1164
+ throw new Error(`npm requires a one-time password for ${packageName}. Pass --otp <code> in non-interactive shells.`);
741
1165
  }
742
1166
  const mutedOutput = new Writable({
743
1167
  write(_chunk, _encoding, callback) {