@detergent-software/atk 8.9.0-dev.3 → 8.9.0-dev.31

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 (275) hide show
  1. package/CHANGELOG.md +211 -0
  2. package/README.md +22 -12
  3. package/build/cli.js +18 -6
  4. package/build/cli.js.map +1 -1
  5. package/build/command-manifest.json +1507 -0
  6. package/build/commands/_app.d.ts.map +1 -1
  7. package/build/commands/_app.js +8 -4
  8. package/build/commands/_app.js.map +1 -1
  9. package/build/commands/audit.d.ts.map +1 -1
  10. package/build/commands/audit.js +1 -1
  11. package/build/commands/audit.js.map +1 -1
  12. package/build/commands/browse.js +2 -2
  13. package/build/commands/browse.js.map +1 -1
  14. package/build/commands/cache.d.ts +1 -1
  15. package/build/commands/cache.d.ts.map +1 -1
  16. package/build/commands/cache.js +1 -16
  17. package/build/commands/cache.js.map +1 -1
  18. package/build/commands/config.d.ts.map +1 -1
  19. package/build/commands/config.js +1 -1
  20. package/build/commands/config.js.map +1 -1
  21. package/build/commands/diff.d.ts.map +1 -1
  22. package/build/commands/diff.js +4 -0
  23. package/build/commands/diff.js.map +1 -1
  24. package/build/commands/drift.d.ts.map +1 -1
  25. package/build/commands/drift.js +4 -0
  26. package/build/commands/drift.js.map +1 -1
  27. package/build/commands/index.d.ts.map +1 -1
  28. package/build/commands/index.js +5 -40
  29. package/build/commands/index.js.map +1 -1
  30. package/build/commands/info.d.ts.map +1 -1
  31. package/build/commands/info.js +50 -33
  32. package/build/commands/info.js.map +1 -1
  33. package/build/commands/init.d.ts.map +1 -1
  34. package/build/commands/init.js +4 -2
  35. package/build/commands/init.js.map +1 -1
  36. package/build/commands/install.d.ts.map +1 -1
  37. package/build/commands/install.js +28 -7
  38. package/build/commands/install.js.map +1 -1
  39. package/build/commands/list.js +2 -2
  40. package/build/commands/list.js.map +1 -1
  41. package/build/commands/login.d.ts.map +1 -1
  42. package/build/commands/login.js +12 -2
  43. package/build/commands/login.js.map +1 -1
  44. package/build/commands/logout.d.ts.map +1 -1
  45. package/build/commands/logout.js +9 -1
  46. package/build/commands/logout.js.map +1 -1
  47. package/build/commands/outdated.js +1 -1
  48. package/build/commands/outdated.js.map +1 -1
  49. package/build/commands/prune.d.ts.map +1 -1
  50. package/build/commands/prune.js +4 -2
  51. package/build/commands/prune.js.map +1 -1
  52. package/build/commands/publish.d.ts +1 -1
  53. package/build/commands/publish.d.ts.map +1 -1
  54. package/build/commands/publish.js +31 -36
  55. package/build/commands/publish.js.map +1 -1
  56. package/build/commands/release-notes.d.ts.map +1 -1
  57. package/build/commands/release-notes.js +4 -0
  58. package/build/commands/release-notes.js.map +1 -1
  59. package/build/commands/schema.d.ts +3 -3
  60. package/build/commands/search.js +1 -1
  61. package/build/commands/search.js.map +1 -1
  62. package/build/commands/sync.d.ts +1 -1
  63. package/build/commands/sync.d.ts.map +1 -1
  64. package/build/commands/sync.js +27 -12
  65. package/build/commands/sync.js.map +1 -1
  66. package/build/commands/update.d.ts.map +1 -1
  67. package/build/commands/update.js +58 -9
  68. package/build/commands/update.js.map +1 -1
  69. package/build/commands/whoami.d.ts.map +1 -1
  70. package/build/commands/whoami.js +16 -4
  71. package/build/commands/whoami.js.map +1 -1
  72. package/build/commands/why.js +3 -4
  73. package/build/commands/why.js.map +1 -1
  74. package/build/components/Header.d.ts +2 -3
  75. package/build/components/Header.d.ts.map +1 -1
  76. package/build/components/Header.js +2 -9
  77. package/build/components/Header.js.map +1 -1
  78. package/build/components/LocalEditsHint.d.ts +16 -0
  79. package/build/components/LocalEditsHint.d.ts.map +1 -0
  80. package/build/components/LocalEditsHint.js +18 -0
  81. package/build/components/LocalEditsHint.js.map +1 -0
  82. package/build/components/SyncAllInSync.d.ts +5 -1
  83. package/build/components/SyncAllInSync.d.ts.map +1 -1
  84. package/build/components/SyncAllInSync.js +2 -2
  85. package/build/components/SyncAllInSync.js.map +1 -1
  86. package/build/components/header-art.d.ts +7 -0
  87. package/build/components/header-art.d.ts.map +1 -0
  88. package/build/components/header-art.js +14 -0
  89. package/build/components/header-art.js.map +1 -0
  90. package/build/components/index.d.ts +1 -0
  91. package/build/components/index.d.ts.map +1 -1
  92. package/build/components/index.js +1 -0
  93. package/build/components/index.js.map +1 -1
  94. package/build/hooks/useListActions.d.ts.map +1 -1
  95. package/build/hooks/useListActions.js +4 -0
  96. package/build/hooks/useListActions.js.map +1 -1
  97. package/build/hooks/usePruneState.d.ts +1 -7
  98. package/build/hooks/usePruneState.d.ts.map +1 -1
  99. package/build/hooks/usePruneState.js +18 -62
  100. package/build/hooks/usePruneState.js.map +1 -1
  101. package/build/hooks/usePublishState.d.ts +43 -8
  102. package/build/hooks/usePublishState.d.ts.map +1 -1
  103. package/build/hooks/usePublishState.js +82 -565
  104. package/build/hooks/usePublishState.js.map +1 -1
  105. package/build/lib/adapter.d.ts +10 -1
  106. package/build/lib/adapter.d.ts.map +1 -1
  107. package/build/lib/adapter.js +46 -3
  108. package/build/lib/adapter.js.map +1 -1
  109. package/build/lib/api-client.d.ts +9 -5
  110. package/build/lib/api-client.d.ts.map +1 -1
  111. package/build/lib/api-client.js +35 -8
  112. package/build/lib/api-client.js.map +1 -1
  113. package/build/lib/atk-api/generated/types.gen.d.ts +4 -4
  114. package/build/lib/auth.d.ts +12 -2
  115. package/build/lib/auth.d.ts.map +1 -1
  116. package/build/lib/auth.js +38 -3
  117. package/build/lib/auth.js.map +1 -1
  118. package/build/lib/checksum.d.ts +40 -14
  119. package/build/lib/checksum.d.ts.map +1 -1
  120. package/build/lib/checksum.js +110 -57
  121. package/build/lib/checksum.js.map +1 -1
  122. package/build/lib/config.d.ts +4 -4
  123. package/build/lib/config.d.ts.map +1 -1
  124. package/build/lib/config.js +31 -8
  125. package/build/lib/config.js.map +1 -1
  126. package/build/lib/diagnostics.d.ts.map +1 -1
  127. package/build/lib/diagnostics.js +10 -7
  128. package/build/lib/diagnostics.js.map +1 -1
  129. package/build/lib/diff-command.js +1 -1
  130. package/build/lib/diff-command.js.map +1 -1
  131. package/build/lib/errors.d.ts +4 -1
  132. package/build/lib/errors.d.ts.map +1 -1
  133. package/build/lib/errors.js.map +1 -1
  134. package/build/lib/format-age.d.ts +3 -0
  135. package/build/lib/format-age.d.ts.map +1 -0
  136. package/build/lib/format-age.js +18 -0
  137. package/build/lib/format-age.js.map +1 -0
  138. package/build/lib/installer.d.ts +8 -11
  139. package/build/lib/installer.d.ts.map +1 -1
  140. package/build/lib/installer.js +83 -28
  141. package/build/lib/installer.js.map +1 -1
  142. package/build/lib/local-edits.d.ts +72 -0
  143. package/build/lib/local-edits.d.ts.map +1 -0
  144. package/build/lib/local-edits.js +145 -0
  145. package/build/lib/local-edits.js.map +1 -0
  146. package/build/lib/lockfile.d.ts +12 -8
  147. package/build/lib/lockfile.d.ts.map +1 -1
  148. package/build/lib/lockfile.js +209 -59
  149. package/build/lib/lockfile.js.map +1 -1
  150. package/build/lib/mover.d.ts.map +1 -1
  151. package/build/lib/mover.js +11 -12
  152. package/build/lib/mover.js.map +1 -1
  153. package/build/lib/multi-tool-install.d.ts +4 -2
  154. package/build/lib/multi-tool-install.d.ts.map +1 -1
  155. package/build/lib/multi-tool-install.js +12 -2
  156. package/build/lib/multi-tool-install.js.map +1 -1
  157. package/build/lib/offline.d.ts +19 -0
  158. package/build/lib/offline.d.ts.map +1 -0
  159. package/build/lib/offline.js +26 -0
  160. package/build/lib/offline.js.map +1 -0
  161. package/build/lib/owned-paths.d.ts +7 -4
  162. package/build/lib/owned-paths.d.ts.map +1 -1
  163. package/build/lib/owned-paths.js +23 -8
  164. package/build/lib/owned-paths.js.map +1 -1
  165. package/build/lib/parse-json.d.ts +12 -0
  166. package/build/lib/parse-json.d.ts.map +1 -1
  167. package/build/lib/parse-json.js +15 -2
  168. package/build/lib/parse-json.js.map +1 -1
  169. package/build/lib/pastel/commander-specs.d.ts +7 -0
  170. package/build/lib/pastel/commander-specs.d.ts.map +1 -0
  171. package/build/lib/pastel/commander-specs.js +50 -0
  172. package/build/lib/pastel/commander-specs.js.map +1 -0
  173. package/build/lib/pastel/generate-arguments.d.ts +3 -3
  174. package/build/lib/pastel/generate-arguments.d.ts.map +1 -1
  175. package/build/lib/pastel/generate-arguments.js +25 -22
  176. package/build/lib/pastel/generate-arguments.js.map +1 -1
  177. package/build/lib/pastel/generate-command.d.ts +7 -5
  178. package/build/lib/pastel/generate-command.d.ts.map +1 -1
  179. package/build/lib/pastel/generate-command.js +32 -67
  180. package/build/lib/pastel/generate-command.js.map +1 -1
  181. package/build/lib/pastel/generate-commands.d.ts +3 -5
  182. package/build/lib/pastel/generate-commands.d.ts.map +1 -1
  183. package/build/lib/pastel/generate-commands.js +6 -10
  184. package/build/lib/pastel/generate-commands.js.map +1 -1
  185. package/build/lib/pastel/generate-options.d.ts +3 -3
  186. package/build/lib/pastel/generate-options.d.ts.map +1 -1
  187. package/build/lib/pastel/generate-options.js +16 -13
  188. package/build/lib/pastel/generate-options.js.map +1 -1
  189. package/build/lib/pastel/index.d.ts +26 -4
  190. package/build/lib/pastel/index.d.ts.map +1 -1
  191. package/build/lib/pastel/index.js +104 -18
  192. package/build/lib/pastel/index.js.map +1 -1
  193. package/build/lib/pastel/internal-types.d.ts +39 -8
  194. package/build/lib/pastel/internal-types.d.ts.map +1 -1
  195. package/build/lib/pastel/manifest.d.ts +8 -0
  196. package/build/lib/pastel/manifest.d.ts.map +1 -0
  197. package/build/lib/pastel/manifest.js +7 -0
  198. package/build/lib/pastel/manifest.js.map +1 -0
  199. package/build/lib/pastel/read-commands.d.ts +7 -2
  200. package/build/lib/pastel/read-commands.d.ts.map +1 -1
  201. package/build/lib/pastel/read-commands.js +51 -30
  202. package/build/lib/pastel/read-commands.js.map +1 -1
  203. package/build/lib/pastel/render-command.d.ts +13 -0
  204. package/build/lib/pastel/render-command.d.ts.map +1 -0
  205. package/build/lib/pastel/render-command.js +56 -0
  206. package/build/lib/pastel/render-command.js.map +1 -0
  207. package/build/lib/process-alive.d.ts +8 -0
  208. package/build/lib/process-alive.d.ts.map +1 -0
  209. package/build/lib/process-alive.js +16 -0
  210. package/build/lib/process-alive.js.map +1 -0
  211. package/build/lib/pruner.d.ts +26 -0
  212. package/build/lib/pruner.d.ts.map +1 -0
  213. package/build/lib/pruner.js +42 -0
  214. package/build/lib/pruner.js.map +1 -0
  215. package/build/lib/publish-flow.d.ts +7 -15
  216. package/build/lib/publish-flow.d.ts.map +1 -1
  217. package/build/lib/publish-flow.js +634 -178
  218. package/build/lib/publish-flow.js.map +1 -1
  219. package/build/lib/publish-run.d.ts +55 -0
  220. package/build/lib/publish-run.d.ts.map +1 -0
  221. package/build/lib/publish-run.js +169 -0
  222. package/build/lib/publish-run.js.map +1 -0
  223. package/build/lib/publisher.d.ts +23 -16
  224. package/build/lib/publisher.d.ts.map +1 -1
  225. package/build/lib/publisher.js +34 -35
  226. package/build/lib/publisher.js.map +1 -1
  227. package/build/lib/registrar.d.ts.map +1 -1
  228. package/build/lib/registrar.js +2 -2
  229. package/build/lib/registrar.js.map +1 -1
  230. package/build/lib/registry.d.ts +18 -7
  231. package/build/lib/registry.d.ts.map +1 -1
  232. package/build/lib/registry.js +113 -32
  233. package/build/lib/registry.js.map +1 -1
  234. package/build/lib/root-path.d.ts +11 -0
  235. package/build/lib/root-path.d.ts.map +1 -1
  236. package/build/lib/root-path.js +15 -1
  237. package/build/lib/root-path.js.map +1 -1
  238. package/build/lib/schemas/config.d.ts +3 -1
  239. package/build/lib/schemas/config.d.ts.map +1 -1
  240. package/build/lib/schemas/config.js +1 -1
  241. package/build/lib/schemas/config.js.map +1 -1
  242. package/build/lib/schemas/lockfile.d.ts +156 -23
  243. package/build/lib/schemas/lockfile.d.ts.map +1 -1
  244. package/build/lib/schemas/lockfile.js +100 -79
  245. package/build/lib/schemas/lockfile.js.map +1 -1
  246. package/build/lib/setup.d.ts.map +1 -1
  247. package/build/lib/setup.js +53 -48
  248. package/build/lib/setup.js.map +1 -1
  249. package/build/lib/sync.d.ts +22 -6
  250. package/build/lib/sync.d.ts.map +1 -1
  251. package/build/lib/sync.js +59 -7
  252. package/build/lib/sync.js.map +1 -1
  253. package/build/lib/uninstaller.d.ts.map +1 -1
  254. package/build/lib/uninstaller.js +8 -5
  255. package/build/lib/uninstaller.js.map +1 -1
  256. package/build/lib/update-check.d.ts +4 -0
  257. package/build/lib/update-check.d.ts.map +1 -1
  258. package/build/lib/update-check.js +55 -56
  259. package/build/lib/update-check.js.map +1 -1
  260. package/build/lib/updater.d.ts +31 -1
  261. package/build/lib/updater.d.ts.map +1 -1
  262. package/build/lib/updater.js +143 -4
  263. package/build/lib/updater.js.map +1 -1
  264. package/build/lib/usage-error.d.ts +20 -0
  265. package/build/lib/usage-error.d.ts.map +1 -0
  266. package/build/lib/usage-error.js +41 -0
  267. package/build/lib/usage-error.js.map +1 -0
  268. package/build/lib/user-setup.d.ts.map +1 -1
  269. package/build/lib/user-setup.js +62 -59
  270. package/build/lib/user-setup.js.map +1 -1
  271. package/build/lib/write-file.d.ts +5 -0
  272. package/build/lib/write-file.d.ts.map +1 -1
  273. package/build/lib/write-file.js +68 -13
  274. package/build/lib/write-file.js.map +1 -1
  275. package/package.json +81 -81
@@ -1,17 +1,17 @@
1
1
  import { writeFile } from 'node:fs/promises';
2
2
  import { join, resolve } from 'node:path';
3
+ import { compare as semverCompare, valid as semverValid } from 'semver';
3
4
  import { loadAllAdapters, resolveInstalledPaths } from './adapter.js';
4
5
  import { isApiError } from './api-client.js';
5
6
  import { getCredential } from './auth.js';
6
- import { hashFile } from './checksum.js';
7
7
  import { errorGuidance, errorMessage } from './errors.js';
8
- import { detectCompatibleTools, getSignedInUserName } from './init.js';
9
- import { addAssetToLockfile, findInstalledAsset, readLockfile, withLockfileLock, writeLockfile } from './lockfile.js';
8
+ import { detectCompatibleTools, getEntrypointName, getSignedInUserName } from './init.js';
9
+ import { findInstalledAsset, readLockfile } from './lockfile.js';
10
10
  import { diffManifests, fetchExistingManifest } from './manifest-diff.js';
11
11
  import { parseOrgFromName } from './org.js';
12
12
  import { findProjectRoot } from './paths.js';
13
- import { buildPublishPlan, buildStagingArea, checkRegistryVersion, detectAssetFromPath, detectPublishType, executePublish, findSiblingDependencies, loadPublishBatch, loadPublishEntries, mapPublishError, missingDependencies, resolvePlanDependencies, unknownToolsError, updateManifestField, updateManifestVersion, validatePublishTarget, } from './publisher.js';
14
- import { clearCache, fetchAssetManifest, fetchRegistry, findAsset, findBundle } from './registry.js';
13
+ import { buildPublishPlan, buildStagingArea, bumpVersion, checkRegistryVersion, detectAssetFromPath, detectPublishType, executePublish, filledManifestFields, findSiblingDependencies, isOrgScopedTarget, loadPublishBatch, loadPublishEntries, mapPublishError, missingDependencies, readManifestFile, resolvePlanDependencies, unknownToolsError, updateManifestField, updateManifestVersion, validatePublishTarget, } from './publisher.js';
14
+ import { clearCache, fetchAssetFileBytes, fetchAssetReadme, fetchRegistry, findAsset } from './registry.js';
15
15
  import { parseReviewersInput, validateGithubUsername } from './reviewers.js';
16
16
  import { stageInstalledFiles } from './staging.js';
17
17
  import { resolveTools } from './tool-resolver.js';
@@ -63,7 +63,6 @@ export function createInitialState(flags) {
63
63
  target: undefined,
64
64
  token: undefined,
65
65
  toolAdapters: [],
66
- toolCursorIndex: 0,
67
66
  toolsDetected: [],
68
67
  validation: undefined,
69
68
  versionCheck: undefined,
@@ -163,6 +162,64 @@ async function firstFailure(edits) {
163
162
  }
164
163
  return undefined;
165
164
  }
165
+ /**
166
+ * The metadata step of a raw publish, for a raw folder or an installed asset, whose `source`
167
+ * gives its org and any description of its own: the signed-in author, the description from
168
+ * --description or the source, the tags from --tags, and the tools from --tools or detection.
169
+ * An interactive run goes on to ask for what is missing; a non-interactive one stages the
170
+ * manifest now, or fails without a description or tools.
171
+ */
172
+ async function gatherRawMetadata(state, ctx, detection, source) {
173
+ const [signedIn, tools] = await Promise.all([
174
+ getSignedInUserName(),
175
+ state.flags.toolsFlag ? splitList(state.flags.toolsFlag) : detectCompatibleTools(detection.assetType),
176
+ ]);
177
+ if (ctx.cancelled())
178
+ return;
179
+ const author = signedIn ?? '';
180
+ const description = state.flags.descriptionFlag?.trim() || source.description;
181
+ if (!state.nonInteractive) {
182
+ ctx.dispatch({
183
+ authorValue: author,
184
+ ...(description ? { description } : {}),
185
+ orgValue: source.org ?? '',
186
+ rawDetection: detection,
187
+ toolsDetected: tools,
188
+ type: 'RAW_DETECT_DONE',
189
+ });
190
+ return;
191
+ }
192
+ if (!description) {
193
+ ctx.dispatch({
194
+ message: state.flags.fromInstalled
195
+ ? `description required: provide --description (the lockfile has none for '${detection.name}')`
196
+ : 'description required: provide --description or include in manifest.json',
197
+ type: 'ERROR',
198
+ });
199
+ return;
200
+ }
201
+ if (tools.length === 0) {
202
+ ctx.dispatch({
203
+ message: `--tools <comma-separated> required: no tools detected for type ${detection.assetType}`,
204
+ type: 'ERROR',
205
+ });
206
+ return;
207
+ }
208
+ const stagingDir = await buildStagingArea(detection, {
209
+ author,
210
+ description,
211
+ ...(source.org ? { org: source.org } : {}),
212
+ tags: splitList(state.flags.tagsFlag),
213
+ tools,
214
+ });
215
+ ctx.stage(stagingDir);
216
+ if (ctx.cancelled())
217
+ return;
218
+ const detected = await detectPublishType(stagingDir);
219
+ if (ctx.cancelled())
220
+ return;
221
+ ctx.dispatch({ rawDetection: detection, target: detected, type: 'RAW_DETECT_DONE_NON_INTERACTIVE' });
222
+ }
166
223
  function stepFor(state) {
167
224
  switch (state.phase) {
168
225
  case 'building-plan':
@@ -175,8 +232,6 @@ function stepFor(state) {
175
232
  return detecting;
176
233
  case 'gathering-metadata':
177
234
  return state.metadataSubPhase === 'building-staging' ? buildingStaging : undefined;
178
- case 'installing':
179
- return installing;
180
235
  case 'loading-tools':
181
236
  return loadingTools;
182
237
  case 'publishing':
@@ -229,10 +284,10 @@ function toErrorAction(err, override) {
229
284
  }
230
285
  /**
231
286
  * Write what the run changed in memory to the manifests on disk: the root's bump and each
232
- * entry's, and the tags and tools the prompts asked for. Only once the publish has opened its
233
- * PR, so a cancelled or failed one leaves every manifest as it was. A file whose write fails
234
- * takes none of its later edits and comes back as one warning naming the fields it lacks; the
235
- * other files are still written.
287
+ * entry's, and the tags and tools the publish sent on top of the root's manifest (from flags,
288
+ * prompts or detection). Only once the publish has opened its PR, so a cancelled or failed one
289
+ * leaves every manifest as it was. A file whose write fails takes none of its later edits and
290
+ * comes back as one warning naming the fields it lacks; the other files are still written.
236
291
  */
237
292
  async function writeManifestEdits(state) {
238
293
  const target = state.target;
@@ -244,19 +299,14 @@ async function writeManifestEdits(state) {
244
299
  files.set(path, [...(files.get(path) ?? []), { field, value, write }]);
245
300
  };
246
301
  const version = (dir, file, value) => edit(dir, file, 'version', value, () => updateManifestVersion(dir, file, value));
247
- const field = (name, value) => edit(target.sourceDir, 'manifest.json', name, value, () => updateManifestField(target.sourceDir, 'manifest.json', name, value));
302
+ const manifestFile = target.type === 'asset' ? 'manifest.json' : 'bundle.json';
248
303
  if (state.versionCheck?.status === 'needs-bump' && state.resolvedVersion) {
249
- version(target.sourceDir, target.type === 'asset' ? 'manifest.json' : 'bundle.json', state.resolvedVersion);
304
+ version(target.sourceDir, manifestFile, state.resolvedVersion);
250
305
  }
251
- // Only an asset's manifest.json takes the prompted tags and tools, as the target (and so the plan) has them.
252
- if (target.type === 'asset') {
253
- const { tags, tools } = target.data;
254
- if (state.promptTagsValue && tags && tags.length > 0) {
255
- field('tags', tags);
256
- }
257
- if (state.selectedPromptTools.length > 0) {
258
- field('tools', tools);
259
- }
306
+ // A manifest that can't be read now takes no field edits; its version edit, if any, warns.
307
+ const written = await readManifestFile(target).catch(() => undefined);
308
+ for (const [name, value] of Object.entries(written ? filledManifestFields(written, target) : {})) {
309
+ edit(target.sourceDir, manifestFile, name, value, () => updateManifestField(target.sourceDir, manifestFile, name, value));
260
310
  }
261
311
  for (const entry of state.entries) {
262
312
  if (entry.versionCheck?.status === 'needs-bump' && entry.resolvedVersion) {
@@ -269,7 +319,7 @@ async function writeManifestEdits(state) {
269
319
  if (!failed)
270
320
  continue;
271
321
  const missing = edits.slice(failed.index);
272
- // Only the version matters to the next publish: it asks for missing tags and tools again.
322
+ // Only the version matters to the next publish: it fills missing tags and tools again.
273
323
  const again = missing.some((e) => e.field === 'version') ? ' before you publish it again' : '';
274
324
  unwritten.push(`The PR is open, but ${path} wasn't updated (${errorMessage(failed.error)}): set ${missing.map((e) => `"${e.field}" to ${JSON.stringify(e.value)}`).join(', ')}${again}.`);
275
325
  }
@@ -278,29 +328,45 @@ async function writeManifestEdits(state) {
278
328
  // ---------------------------------------------------------------------------
279
329
  // Steps, one per phase that waits on work
280
330
  // ---------------------------------------------------------------------------
331
+ /**
332
+ * The installed asset's files, at their source paths, through a raw publish's metadata step,
333
+ * with the version, entrypoint, dependencies, org and description its lockfile entry records.
334
+ */
281
335
  const resolvingInstalled = {
282
336
  async run(state, ctx) {
283
337
  const source = await findInstalledSource(state, ctx);
284
338
  if (!source)
285
339
  return;
286
340
  const { adapter, installed, projectRoot } = source;
287
- // Copy the installed files into a temp directory, preserving their source paths
288
- const tempDir = await stageInstalledFiles(installed, adapter, projectRoot);
289
- ctx.stage(tempDir);
290
- if (ctx.cancelled())
291
- return;
292
- // Generate manifest.json in the temp dir
293
- const manifest = {
294
- author: 'unknown',
295
- description: `Published from installed asset ${installed.name}`,
296
- entrypoint: installed.files[0]?.sourcePath ?? 'index.md',
341
+ const { files } = resolveInstalledPaths(installed, adapter, projectRoot);
342
+ // An older lockfile entry has no entrypoint: the type's usual one when it was installed, else its first file.
343
+ const usual = getEntrypointName(installed.type, installed.name);
344
+ const entrypoint = installed.entrypoint ?? (files.some((file) => file.sourcePath === usual) ? usual : files[0]?.sourcePath);
345
+ const dependencies = (installed.dependencies ?? []).map(({ name, type, version }) => ({
346
+ name,
347
+ type,
348
+ ...(version ? { version } : {}),
349
+ }));
350
+ const detection = {
351
+ assetType: installed.type,
352
+ ...(dependencies.length > 0 ? { dependencies } : {}),
353
+ ...(entrypoint ? { entrypoint } : {}),
354
+ files: files.map((file) => ({
355
+ absolutePath: join(projectRoot, file.installedPath),
356
+ relativePath: file.sourcePath,
357
+ })),
297
358
  name: installed.name,
298
- ...(installed.org ? { org: installed.org } : {}),
299
- type: installed.type,
359
+ sourceDir: projectRoot,
300
360
  version: installed.version,
301
361
  };
302
- await writeFile(join(tempDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
303
- ctx.dispatch({ resolvedPath: tempDir, type: 'RESOLVED_INSTALLED' });
362
+ // A --with folder that is missing, a bundle or a repeat fails here, before any prompt, as in detecting.
363
+ if (state.flags.withPaths?.length) {
364
+ const entries = await loadPublishEntries(state.flags.withPaths, projectRoot);
365
+ if (ctx.cancelled())
366
+ return;
367
+ ctx.dispatch({ entries, type: 'ENTRIES_LOADED' });
368
+ }
369
+ await gatherRawMetadata(state, ctx, detection, { description: installed.description, org: installed.org });
304
370
  },
305
371
  };
306
372
  const resolvingUpdate = {
@@ -310,21 +376,35 @@ const resolvingUpdate = {
310
376
  return;
311
377
  const { adapter, installed, org, projectRoot } = source;
312
378
  const assetName = state.flags.rawPath;
313
- // Fetch the registry to find the latest version
379
+ // Fetch the registry to find the latest version. An installed entry is always an asset.
314
380
  const registry = await fetchRegistry({ force: state.flags.refresh });
315
381
  if (ctx.cancelled())
316
382
  return;
317
- // Look up the asset in the registry: try findAsset first, then findBundle
318
383
  const resolvedAsset = findAsset(registry, installed.name, installed.type, undefined, installed.org ?? org);
319
- const resolvedBundle = resolvedAsset ? undefined : findBundle(registry, installed.name, installed.org ?? org);
320
- if (!resolvedAsset && !resolvedBundle) {
384
+ if (!resolvedAsset) {
321
385
  ctx.dispatch({ message: `Asset '${assetName}' not found in the registry.`, type: 'ERROR' });
322
386
  return;
323
387
  }
324
- const latestVersion = resolvedAsset ? resolvedAsset.version : resolvedBundle.version;
325
- const assetType = resolvedAsset ? resolvedAsset.asset.type : 'bundle';
326
- // Fetch the authoritative manifest (and optional README) from the registry
327
- const fetched = await fetchAssetManifest({ name: installed.name, org: installed.org ?? org, type: assetType }, latestVersion);
388
+ // The installed files go out on top of the latest's manifest, so they must not be older
389
+ // than it: they would publish the next version without the changes made since.
390
+ const latestVersion = resolvedAsset.version;
391
+ if (!semverValid(installed.version) || semverCompare(installed.version, latestVersion) < 0) {
392
+ ctx.dispatch({
393
+ command: `atk diff ${assetName}`,
394
+ hint: `See what changed since v${installed.version} with \`atk diff ${assetName}\`, get the latest into a folder with \`atk checkout ${assetName}\`, carry your edits into it, and publish that folder.`,
395
+ message: `'${assetName}' is installed at v${installed.version}, but the registry's latest is v${latestVersion}. --update would publish your copy, built on v${installed.version}, as the next version and undo the changes made since.`,
396
+ type: 'ERROR',
397
+ });
398
+ return;
399
+ }
400
+ // The latest's manifest as the registry has it, byte for byte, so a field this CLI's schema
401
+ // doesn't know (from a newer publisher) is published again; and its README, if any.
402
+ const { org: assetOrg, type } = resolvedAsset.asset;
403
+ const ref = { name: installed.name, ...(assetOrg ? { org: assetOrg } : {}), type };
404
+ const [manifest, readme] = await Promise.all([
405
+ fetchAssetFileBytes(ref, latestVersion, 'manifest.json'),
406
+ fetchAssetReadme(ref, latestVersion),
407
+ ]);
328
408
  if (ctx.cancelled())
329
409
  return;
330
410
  // Copy the user's locally modified files into a temp staging directory
@@ -332,13 +412,9 @@ const resolvingUpdate = {
332
412
  ctx.stage(tempDir);
333
413
  if (ctx.cancelled())
334
414
  return;
335
- // Write the fetched manifest into the staging directory. It is the manifest as this
336
- // CLI's schema parsed it: a field the schema does not know (from a newer publisher)
337
- // is dropped here and so is missing from the re-published version.
338
- await writeFile(join(tempDir, fetched.manifestFileName), fetched.manifestContent, 'utf-8');
339
- // Write README if present
340
- if (fetched.readmeContent) {
341
- await writeFile(join(tempDir, 'README.md'), fetched.readmeContent, 'utf-8');
415
+ await writeFile(join(tempDir, 'manifest.json'), manifest);
416
+ if (readme) {
417
+ await writeFile(join(tempDir, 'README.md'), readme, 'utf-8');
342
418
  }
343
419
  ctx.dispatch({ resolvedPath: tempDir, type: 'RESOLVED_UPDATE' });
344
420
  },
@@ -389,66 +465,11 @@ const detecting = {
389
465
  const detection = await detectAssetFromPath(state.resolvedPath, adapter, projectRoot);
390
466
  if (ctx.cancelled())
391
467
  return;
392
- // Pre-fill metadata
393
- const author = (await getSignedInUserName()) ?? '';
394
- if (ctx.cancelled())
395
- return;
468
+ // No manifest exists yet, so the project's org and the flags and prompts give the metadata.
396
469
  const lockfile = await readLockfile(projectRoot);
397
470
  if (ctx.cancelled())
398
471
  return;
399
- const tools = await detectCompatibleTools(detection.assetType);
400
- if (ctx.cancelled())
401
- return;
402
- // Non-interactive mode (raw publish — no manifest exists yet, so the only
403
- // sources for description are --description flag or exit-1; tags come from
404
- // --tags or default to empty). Tool detection runs for both modes above.
405
- if (!state.nonInteractive) {
406
- // Interactive mode — gather metadata from the user
407
- ctx.dispatch({
408
- authorValue: author,
409
- orgValue: lockfile.org ?? '',
410
- rawDetection: detection,
411
- toolsDetected: tools,
412
- type: 'RAW_DETECT_DONE',
413
- });
414
- return;
415
- }
416
- if (!state.flags.descriptionFlag) {
417
- ctx.dispatch({
418
- message: 'description required: provide --description or include in manifest.json',
419
- type: 'ERROR',
420
- });
421
- return;
422
- }
423
- // Tools resolution: --tools flag -> detected (already populated above)
424
- let toolsList;
425
- if (state.flags.toolsFlag) {
426
- toolsList = splitList(state.flags.toolsFlag);
427
- }
428
- else if (tools.length > 0) {
429
- toolsList = tools;
430
- }
431
- else {
432
- ctx.dispatch({
433
- message: `--tools <comma-separated> required: no tools detected for type ${detection.assetType}`,
434
- type: 'ERROR',
435
- });
436
- return;
437
- }
438
- const stagingDir = await buildStagingArea(detection, {
439
- author,
440
- description: state.flags.descriptionFlag,
441
- ...(lockfile.org ? { org: lockfile.org } : {}),
442
- tags: splitList(state.flags.tagsFlag),
443
- tools: toolsList,
444
- });
445
- ctx.stage(stagingDir);
446
- if (ctx.cancelled())
447
- return;
448
- const detected = await detectPublishType(stagingDir);
449
- if (ctx.cancelled())
450
- return;
451
- ctx.dispatch({ rawDetection: detection, target: detected, type: 'RAW_DETECT_DONE_NON_INTERACTIVE' });
472
+ await gatherRawMetadata(state, ctx, detection, { org: lockfile.org });
452
473
  },
453
474
  };
454
475
  /** The `building-staging` sub-phase of `gathering-metadata`: the manifest from the prompts' answers. */
@@ -529,14 +550,13 @@ const validating = {
529
550
  },
530
551
  };
531
552
  /**
532
- * In interactive mode, loads adapters for the multi-select prompt. In non-interactive
533
- * mode, resolves tools via manifest -> --tools flag -> detectCompatibleTools(assetType)
534
- * -> exit 1, then dispatches RESOLVE_TOOLS_NONINTERACTIVE to advance directly to
535
- * checking-registry.
553
+ * In interactive mode without --tools, loads adapters for the multi-select prompt. Otherwise
554
+ * resolves tools via manifest -> --tools flag -> detectCompatibleTools(assetType) -> exit 1,
555
+ * then dispatches RESOLVE_TOOLS_NONINTERACTIVE to advance past the prompt.
536
556
  */
537
557
  const loadingTools = {
538
558
  async run(state, ctx) {
539
- if (!state.nonInteractive) {
559
+ if (!state.nonInteractive && !state.flags.toolsFlag) {
540
560
  const adapters = await loadAllAdapters();
541
561
  if (ctx.cancelled())
542
562
  return;
@@ -690,8 +710,8 @@ const buildingPlan = {
690
710
  const localPlan = await buildPublishPlan(currentTarget, currentVersion, state.isUpdate, state.flags.message, reviewers, entries, ...(isBatch(state.flags) ? [state.flags.rawPath] : []));
691
711
  if (ctx.cancelled())
692
712
  return;
693
- // Resolve declared dependencies against the registry now, so the preview and
694
- // --dry-run show them and an unresolvable one stops before the confirm prompt.
713
+ // Ask the API for its plan now, so the preview and --dry-run show its dependencies
714
+ // and reviewers, and anything it rejects stops before the confirm prompt.
695
715
  const publishPlan = await resolvePlanDependencies(localPlan);
696
716
  if (ctx.cancelled())
697
717
  return;
@@ -728,75 +748,511 @@ const publishing = {
728
748
  catch {
729
749
  // Best-effort — don't fail publish over cache
730
750
  }
731
- // Update lockfile in-place when publishing from an installed asset
732
- if (state.flags.fromInstalled || state.flags.update) {
733
- try {
734
- const projectRoot = findProjectRoot(state.flags.projectRoot);
735
- const pubTools = await resolveTools(state.flags.tool);
736
- const pubAdapter = pubTools[0].adapter;
737
- await withLockfileLock(projectRoot, async () => {
738
- const lockfile = await readLockfile(projectRoot);
739
- const { name, org } = parseOrgFromName(state.flags.rawPath);
740
- const installed = findInstalledAsset(lockfile, name, undefined, org);
741
- if (installed) {
742
- const resolved = resolveInstalledPaths(installed, pubAdapter, projectRoot);
743
- const updatedFiles = await Promise.all(resolved.files.map(async (file) => ({
744
- checksum: await hashFile(join(projectRoot, file.installedPath)),
745
- sourcePath: file.sourcePath,
746
- })));
747
- const updatedLockfile = addAssetToLockfile(lockfile, {
748
- ...installed,
749
- files: updatedFiles,
750
- version: state.resolvedVersion,
751
- });
752
- await writeLockfile(projectRoot, updatedLockfile);
751
+ // The lockfile is left as it was, even for an installed asset or a raw folder: the registry
752
+ // has the new version only once the PR merges, and a fresh clone installs the version the
753
+ // lockfile names (ATK-592). `atk update` (or `atk install`) records it after the merge.
754
+ if (ctx.cancelled())
755
+ return;
756
+ ctx.dispatch({ result, type: 'PUBLISH_DONE' });
757
+ },
758
+ };
759
+ // ---------------------------------------------------------------------------
760
+ // Reducer
761
+ // ---------------------------------------------------------------------------
762
+ export function publishReducer(state, action) {
763
+ switch (action.type) {
764
+ case 'BUMP_DONE': {
765
+ const bumped = {
766
+ ...state,
767
+ errorMessage: action.message ? action.message : state.errorMessage,
768
+ resolvedVersion: action.version,
769
+ target: action.updatedTarget,
770
+ };
771
+ return action.message ? { ...bumped, phase: 'error' } : advanceToPlan(bumped);
772
+ }
773
+ case 'CONFIRM_PROMPT_TOOLS': {
774
+ if (action.tools.length === 0)
775
+ return state;
776
+ const updatedToolsTarget = state.target
777
+ ? {
778
+ ...state.target,
779
+ data: { ...state.target.data, tools: action.tools },
780
+ }
781
+ : undefined;
782
+ return {
783
+ ...state,
784
+ phase: phaseAfterMetadata(updatedToolsTarget),
785
+ selectedPromptTools: action.tools,
786
+ target: updatedToolsTarget,
787
+ };
788
+ }
789
+ case 'DETECT_DONE':
790
+ if (action.target.type === 'bundle' && state.entries.length > 0) {
791
+ return {
792
+ ...state,
793
+ errorMessage: 'A bundle is published on its own. Remove --with, and publish its new assets first.',
794
+ phase: 'error',
795
+ target: action.target,
796
+ };
797
+ }
798
+ return {
799
+ ...state,
800
+ phase: 'validating',
801
+ target: action.target,
802
+ };
803
+ case 'ENTRIES_CHECKED':
804
+ return advanceToPlan({ ...state, entries: action.entries });
805
+ case 'ENTRIES_LOADED':
806
+ return { ...state, entries: action.entries };
807
+ case 'ERROR':
808
+ return {
809
+ ...state,
810
+ errorCode: action.code,
811
+ errorCommand: action.command,
812
+ errorHint: action.hint,
813
+ errorMessage: action.message,
814
+ phase: 'error',
815
+ };
816
+ case 'PLAN_DONE': {
817
+ // dry-run always shows the dry-run result; --yes / non-TTY skip the confirm prompt
818
+ // and proceed directly to publishing.
819
+ let nextPhase;
820
+ if (state.flags.dryRun) {
821
+ nextPhase = 'dry-run-result';
822
+ }
823
+ else if (state.nonInteractive) {
824
+ nextPhase = 'publishing';
825
+ }
826
+ else {
827
+ nextPhase = 'confirm-publish';
828
+ }
829
+ return {
830
+ ...state,
831
+ manifestDiff: action.manifestDiff,
832
+ phase: nextPhase,
833
+ plan: action.plan,
834
+ };
835
+ }
836
+ case 'PUBLISH_DONE':
837
+ return { ...state, phase: 'done', publishResult: action.result };
838
+ case 'PUBLISH_PROGRESS':
839
+ return { ...state, progressMessage: action.message };
840
+ case 'RAW_DETECT_DONE':
841
+ // Ask only for what neither a flag nor the installed asset gave.
842
+ return {
843
+ ...state,
844
+ authorValue: action.authorValue,
845
+ descriptionValue: action.description ?? '',
846
+ isRawPublish: !state.flags.fromInstalled,
847
+ metadataSubPhase: action.description === undefined ? 'description' : metadataAfterDescription(state),
848
+ orgValue: action.orgValue,
849
+ phase: 'gathering-metadata',
850
+ rawDetection: action.rawDetection,
851
+ tagsValue: state.flags.tagsFlag ?? '',
852
+ toolsDetected: action.toolsDetected,
853
+ };
854
+ case 'RAW_DETECT_DONE_NON_INTERACTIVE':
855
+ return {
856
+ ...state,
857
+ isRawPublish: !state.flags.fromInstalled,
858
+ phase: 'validating',
859
+ rawDetection: action.rawDetection,
860
+ target: action.target,
861
+ };
862
+ case 'REGISTRY_CHECK_DONE': {
863
+ const { isUpdate, resolvedVersion, token, versionCheck } = action;
864
+ let nextPhase;
865
+ let errorMessage = state.errorMessage;
866
+ let bumpedVersion = resolvedVersion;
867
+ let bumpedTarget = state.target;
868
+ if (versionCheck.status === 'new-asset') {
869
+ nextPhase = 'building-plan';
870
+ }
871
+ else if (versionCheck.status === 'version-already-bumped') {
872
+ // --yes / non-TTY implicitly confirm the update
873
+ nextPhase = state.nonInteractive ? 'building-plan' : 'confirm-update';
874
+ }
875
+ else {
876
+ // needs-bump
877
+ if (state.flags.bump && state.target) {
878
+ // --bump applies in any run; a successful publish writes it to the manifest
879
+ try {
880
+ bumpedVersion = bumpVersion(versionCheck.latestVersion, state.flags.bump);
881
+ bumpedTarget = {
882
+ ...state.target,
883
+ data: { ...state.target.data, version: bumpedVersion },
884
+ };
885
+ nextPhase = 'building-plan';
886
+ }
887
+ catch {
888
+ errorMessage = 'Failed to compute bumped version.';
889
+ nextPhase = 'error';
753
890
  }
891
+ }
892
+ else if (!state.nonInteractive) {
893
+ nextPhase = 'bump-prompt';
894
+ }
895
+ else if (isBatch(state.flags)) {
896
+ // advanceToPlan lists every asset of the batch that needs --bump, once the entries are checked.
897
+ nextPhase = 'building-plan';
898
+ }
899
+ else {
900
+ errorMessage = `--bump <patch|minor|major> required: registry has v${versionCheck.latestVersion} but manifest is not newer`;
901
+ nextPhase = 'error';
902
+ }
903
+ }
904
+ const checked = {
905
+ ...state,
906
+ errorMessage,
907
+ isUpdate,
908
+ phase: nextPhase,
909
+ resolvedVersion: bumpedVersion,
910
+ target: bumpedTarget,
911
+ token,
912
+ versionCheck,
913
+ };
914
+ return nextPhase === 'building-plan' ? advanceToPlan(checked) : checked;
915
+ }
916
+ case 'RESOLVE_TOOLS_NONINTERACTIVE': {
917
+ const updatedTarget = state.target
918
+ ? {
919
+ ...state.target,
920
+ data: { ...state.target.data, tools: action.tools },
921
+ }
922
+ : undefined;
923
+ return {
924
+ ...state,
925
+ // An interactive run's --tools stood in for the tools prompt; the reviewer prompt may still follow.
926
+ phase: state.nonInteractive ? 'checking-registry' : phaseAfterMetadata(updatedTarget),
927
+ target: updatedTarget,
928
+ };
929
+ }
930
+ case 'RESOLVED_UPDATE':
931
+ return {
932
+ ...state,
933
+ phase: 'detecting',
934
+ resolvedPath: action.resolvedPath,
935
+ };
936
+ case 'SET_BUMP_SELECTION':
937
+ return { ...state, bumpSelection: action.value };
938
+ case 'SET_CONFIRM_VALUE':
939
+ return { ...state, confirmValue: action.value };
940
+ case 'SET_DESCRIPTION':
941
+ return { ...state, descriptionValue: action.value };
942
+ case 'SET_PROMPT_TAGS':
943
+ return { ...state, promptTagsValue: action.value };
944
+ case 'SET_PUBLISH_CONFIRM_VALUE':
945
+ return { ...state, publishConfirmValue: action.value };
946
+ case 'SET_REVIEWER':
947
+ return { ...state, reviewerValue: action.value };
948
+ case 'SET_TAGS':
949
+ return { ...state, tagsValue: action.value };
950
+ case 'SET_WITH_CONFIRM_VALUE':
951
+ return { ...state, withConfirmValue: action.value };
952
+ case 'STAGING_DONE':
953
+ return {
954
+ ...state,
955
+ phase: 'validating',
956
+ target: action.target,
957
+ };
958
+ case 'SUBMIT_BUMP': {
959
+ const trimmed = state.bumpSelection.trim().toLowerCase();
960
+ if (trimmed !== 'patch' && trimmed !== 'minor' && trimmed !== 'major') {
961
+ return state; // Ignore invalid input
962
+ }
963
+ // The prompt is for an entry: bump it, then on to the next pending bump or the plan.
964
+ if (state.bumpEntry !== undefined) {
965
+ const entry = state.entries[state.bumpEntry];
966
+ if (entry?.versionCheck?.status !== 'needs-bump')
967
+ return state;
968
+ return bumpEntryAt({ ...state, bumpSelection: '' }, state.bumpEntry, trimmed);
969
+ }
970
+ if (!state.target || !state.versionCheck || state.versionCheck.status !== 'needs-bump') {
971
+ return state;
972
+ }
973
+ // Compute the bumped version in the reducer (pure, synchronous)
974
+ const bumpType = trimmed;
975
+ try {
976
+ const newVersion = bumpVersion(state.versionCheck.latestVersion, bumpType);
977
+ // The bump is only on the target until the publish succeeds, which writes it to
978
+ // the manifest; entries still to check or bump come first.
979
+ return advanceToPlan({
980
+ ...state,
981
+ bumpSelection: '',
982
+ resolvedVersion: newVersion,
983
+ target: { ...state.target, data: { ...state.target.data, version: newVersion } },
754
984
  });
755
985
  }
756
986
  catch {
757
- // Best-effort — don't fail publish over lockfile update
987
+ return {
988
+ ...state,
989
+ errorMessage: 'Failed to compute bumped version.',
990
+ phase: 'error',
991
+ };
758
992
  }
759
993
  }
760
- if (ctx.cancelled())
761
- return;
762
- ctx.dispatch({ result, type: 'PUBLISH_DONE' });
763
- },
764
- };
765
- /** A raw publish records the asset it published in the lockfile, as installed from the project's own files. */
766
- const installing = {
767
- async run(state, ctx) {
768
- try {
769
- const projectRoot = findProjectRoot(state.flags.projectRoot);
770
- await withLockfileLock(projectRoot, async () => {
771
- // Read current lockfile
772
- const lockfile = await readLockfile(projectRoot);
773
- // Compute checksums from actual project files (not staging copies)
774
- const files = [];
775
- for (const file of state.rawDetection.files) {
776
- const checksum = await hashFile(file.absolutePath);
777
- files.push({
778
- checksum,
779
- sourcePath: file.relativePath,
780
- });
994
+ case 'SUBMIT_CONFIRM': {
995
+ const trimmed = state.confirmValue.trim().toLowerCase();
996
+ if (trimmed === 'y' || trimmed === 'yes') {
997
+ return advanceToPlan(state);
998
+ }
999
+ else if (trimmed === 'n' || trimmed === 'no') {
1000
+ return { ...state, errorMessage: 'Publish cancelled.', phase: 'error' };
1001
+ }
1002
+ // Otherwise ignore (wait for valid input)
1003
+ return state;
1004
+ }
1005
+ case 'SUBMIT_DESCRIPTION': {
1006
+ const trimmed = state.descriptionValue.trim();
1007
+ if (!trimmed)
1008
+ return state; // Don't allow empty description
1009
+ return {
1010
+ ...state,
1011
+ descriptionValue: trimmed,
1012
+ metadataSubPhase: metadataAfterDescription(state),
1013
+ };
1014
+ }
1015
+ case 'SUBMIT_PROMPT_TAGS': {
1016
+ const parsedTags = splitList(state.promptTagsValue);
1017
+ // Update the target's data with the new tags
1018
+ const updatedTagsTarget = state.target
1019
+ ? {
1020
+ ...state.target,
1021
+ data: { ...state.target.data, tags: parsedTags.length > 0 ? parsedTags : undefined },
781
1022
  }
782
- // Build InstalledAsset entry
783
- const installedAsset = {
784
- files,
785
- installReason: 'direct',
786
- name: state.rawDetection.name,
787
- org: state.orgValue || undefined,
788
- type: state.rawDetection.assetType,
789
- version: state.resolvedVersion || state.target?.data.version || '1.0.0',
1023
+ : undefined;
1024
+ // Check if tools are also missing (only for assets)
1025
+ const needsToolsPrompt = updatedTagsTarget?.type === 'asset' &&
1026
+ (!('tools' in updatedTagsTarget.data) ||
1027
+ !Array.isArray(updatedTagsTarget.data.tools) ||
1028
+ (updatedTagsTarget.data.tools?.length ?? 0) === 0);
1029
+ const nextTagsPhase = needsToolsPrompt ? 'loading-tools' : phaseAfterMetadata(updatedTagsTarget);
1030
+ return {
1031
+ ...state,
1032
+ phase: nextTagsPhase,
1033
+ target: updatedTagsTarget,
1034
+ };
1035
+ }
1036
+ case 'SUBMIT_PUBLISH_CONFIRM': {
1037
+ const trimmed = state.publishConfirmValue.trim().toLowerCase();
1038
+ if (trimmed === 'y' || trimmed === 'yes') {
1039
+ return { ...state, phase: 'publishing' };
1040
+ }
1041
+ else if (trimmed === 'n' || trimmed === 'no') {
1042
+ return { ...state, errorMessage: 'Publish cancelled.', phase: 'error' };
1043
+ }
1044
+ // Otherwise ignore (wait for valid input)
1045
+ return state;
1046
+ }
1047
+ case 'SUBMIT_REVIEWER':
1048
+ return { ...state, phase: 'checking-registry' };
1049
+ case 'SUBMIT_TAGS':
1050
+ return {
1051
+ ...state,
1052
+ metadataSubPhase: 'building-staging',
1053
+ };
1054
+ case 'SUBMIT_WITH_CONFIRM': {
1055
+ const trimmed = state.withConfirmValue.trim().toLowerCase();
1056
+ if (trimmed === 'y' || trimmed === 'yes') {
1057
+ return advanceToPlan({
1058
+ ...state,
1059
+ entries: [...state.entries, ...state.suggestedEntries],
1060
+ missingDependencies: [],
1061
+ pendingError: undefined,
1062
+ suggestedEntries: [],
1063
+ withConfirmValue: '',
1064
+ });
1065
+ }
1066
+ if ((trimmed === 'n' || trimmed === 'no') && state.pendingError) {
1067
+ return publishReducer({ ...state, suggestedEntries: [], withConfirmValue: '' }, state.pendingError);
1068
+ }
1069
+ return state;
1070
+ }
1071
+ case 'SUGGEST_ENTRIES':
1072
+ return {
1073
+ ...state,
1074
+ missingDependencies: action.missing,
1075
+ pendingError: action.error,
1076
+ phase: 'confirm-with',
1077
+ suggestedEntries: action.suggested,
1078
+ withConfirmValue: '',
1079
+ };
1080
+ case 'TOOLS_LOADED':
1081
+ return {
1082
+ ...state,
1083
+ phase: 'prompting-tools',
1084
+ toolAdapters: action.adapters,
1085
+ };
1086
+ case 'VALIDATE_DONE': {
1087
+ if (!action.validation.valid) {
1088
+ return {
1089
+ ...state,
1090
+ errorMessage: `Validation failed:\n${action.validation.errors.map((e) => ` - ${e}`).join('\n')}`,
1091
+ phase: 'error',
1092
+ validation: action.validation,
790
1093
  };
791
- // Add to lockfile and write
792
- const updatedLockfile = addAssetToLockfile(lockfile, installedAsset);
793
- await writeLockfile(projectRoot, updatedLockfile);
794
- });
1094
+ }
1095
+ // Seed the reviewers field: the --reviewer flag wins over the project's
1096
+ // lockfile-configured default (org.ts's CLI-flag > lockfile priority chain).
1097
+ const reviewerValue = state.flags.reviewerFlag ?? (action.lockfileOrgReviewers ?? []).join(', ');
1098
+ // Determine next phase based on missing tags/tools. A batch asks for neither: as for
1099
+ // a --with entry, a missing one is only a warning.
1100
+ const batch = isBatch(state.flags);
1101
+ const manifest = state.target?.data;
1102
+ const hasTags = batch || (manifest && 'tags' in manifest && Array.isArray(manifest.tags) && manifest.tags.length > 0);
1103
+ const hasTools = batch ||
1104
+ // Only assets have tools; bundles skip the tools prompt
1105
+ state.target?.type !== 'asset' ||
1106
+ (manifest && 'tools' in manifest && Array.isArray(manifest.tools) && manifest.tools.length > 0);
1107
+ const isOrgScoped = batch
1108
+ ? [state.target, ...state.entries.map((entry) => entry.target)].some((t) => t && isOrgScopedTarget(t))
1109
+ : Boolean(state.target && isOrgScopedTarget(state.target));
1110
+ // Reviewers only apply to org-scoped assets (non-org PRs always go to the registry
1111
+ // maintainer) — fail fast rather than silently ignoring the flag.
1112
+ if (!isOrgScoped && state.flags.reviewerFlag && parseReviewersInput(state.flags.reviewerFlag).length > 0) {
1113
+ return {
1114
+ ...state,
1115
+ errorMessage: 'The --reviewer flag only applies to org-scoped assets. Remove the flag to publish this asset.',
1116
+ phase: 'error',
1117
+ validation: action.validation,
1118
+ };
1119
+ }
1120
+ // A manifest without tags takes them from --tags right here, in any run: a non-interactive
1121
+ // one without the flag publishes none, an interactive one asks. Manifest tags always win.
1122
+ const tagsFromFlag = !hasTags && (state.nonInteractive || state.flags.tagsFlag !== undefined);
1123
+ let nextTarget = state.target;
1124
+ if (tagsFromFlag && nextTarget) {
1125
+ const flagTags = splitList(state.flags.tagsFlag);
1126
+ nextTarget = {
1127
+ ...nextTarget,
1128
+ data: { ...nextTarget.data, tags: flagTags.length > 0 ? flagTags : undefined },
1129
+ };
1130
+ }
1131
+ // Non-interactive path: either route to loading-tools (effect resolves tools async) or
1132
+ // directly to checking-registry. The reviewer prompt never applies here — non-interactive
1133
+ // mode always skips it.
1134
+ if (state.nonInteractive) {
1135
+ let nextPhase;
1136
+ if (!hasTools) {
1137
+ // The loading-tools effect detects whether we're non-interactive and
1138
+ // either resolves tools from the manifest/flag/inference or errors out.
1139
+ nextPhase = 'loading-tools';
1140
+ }
1141
+ else {
1142
+ nextPhase = 'checking-registry';
1143
+ }
1144
+ return {
1145
+ ...state,
1146
+ phase: nextPhase,
1147
+ reviewerValue,
1148
+ target: nextTarget,
1149
+ validation: action.validation,
1150
+ };
1151
+ }
1152
+ let nextPhase;
1153
+ if (!hasTags && !tagsFromFlag) {
1154
+ nextPhase = 'prompting-tags';
1155
+ }
1156
+ else if (!hasTools) {
1157
+ nextPhase = 'loading-tools';
1158
+ }
1159
+ else {
1160
+ nextPhase = isOrgScoped ? 'prompting-reviewer' : 'checking-registry';
1161
+ }
1162
+ return {
1163
+ ...state,
1164
+ phase: nextPhase,
1165
+ reviewerValue,
1166
+ target: nextTarget,
1167
+ validation: action.validation,
1168
+ };
795
1169
  }
796
- catch {
797
- // Publish already succeeded, so just warn and move to done
1170
+ default:
1171
+ return state;
1172
+ }
1173
+ }
1174
+ /**
1175
+ * The step before `building-plan`, once the root asset's own checks are done:
1176
+ * check the entries not yet checked, then settle each entry that needs a bump
1177
+ * (`--bump`; without it, the bump prompt, or an error when non-interactive), then plan.
1178
+ */
1179
+ function advanceToPlan(state) {
1180
+ if (state.entries.some((entry) => !entry.versionCheck)) {
1181
+ return { ...state, bumpEntry: undefined, phase: 'checking-entries' };
1182
+ }
1183
+ // A batch without --bump names every asset that needs one, not just the first.
1184
+ if (isBatch(state.flags) && state.nonInteractive && !state.flags.bump) {
1185
+ const pending = [
1186
+ ...(state.target && state.versionCheck?.status === 'needs-bump' && !state.resolvedVersion
1187
+ ? [{ label: state.flags.rawPath, latestVersion: state.versionCheck.latestVersion, target: state.target }]
1188
+ : []),
1189
+ ...state.entries.flatMap((entry) => entry.versionCheck?.status === 'needs-bump' && entry.resolvedVersion === undefined
1190
+ ? [{ label: entry.label, latestVersion: entry.versionCheck.latestVersion, target: entry.target }]
1191
+ : []),
1192
+ ];
1193
+ if (pending.length > 0) {
1194
+ const lines = pending.map((p) => ` ${p.label}: ${p.target.assetType}:${p.target.data.name} v${p.target.data.version}, registry has v${p.latestVersion}`);
1195
+ return {
1196
+ ...state,
1197
+ errorMessage: `--bump <patch|minor|major> required: these manifests are not newer than the registry:\n${lines.join('\n')}`,
1198
+ phase: 'error',
1199
+ };
798
1200
  }
799
- ctx.dispatch({ type: 'INSTALL_DONE' });
800
- },
801
- };
1201
+ }
1202
+ const index = state.entries.findIndex((entry) => entry.versionCheck?.status === 'needs-bump' && entry.resolvedVersion === undefined);
1203
+ const entry = state.entries[index];
1204
+ if (entry?.versionCheck?.status !== 'needs-bump') {
1205
+ return { ...state, bumpEntry: undefined, phase: 'building-plan' };
1206
+ }
1207
+ if (state.flags.bump) {
1208
+ return bumpEntryAt(state, index, state.flags.bump);
1209
+ }
1210
+ if (!state.nonInteractive) {
1211
+ return { ...state, bumpEntry: index, bumpSelection: '', phase: 'bump-prompt' };
1212
+ }
1213
+ return {
1214
+ ...state,
1215
+ errorMessage: `--bump <patch|minor|major> required: registry has v${entry.versionCheck.latestVersion} of ${entry.target.assetType}:${entry.target.data.name} (from ${entry.label}) but its manifest is not newer`,
1216
+ phase: 'error',
1217
+ };
1218
+ }
1219
+ /**
1220
+ * Bump the entry at `index` from its registry version, then go on toward the
1221
+ * plan; an error state, as for the root, when the version can't be bumped.
1222
+ */
1223
+ function bumpEntryAt(state, index, bump) {
1224
+ const entry = state.entries[index];
1225
+ if (entry?.versionCheck?.status !== 'needs-bump')
1226
+ return state;
1227
+ let version;
1228
+ try {
1229
+ version = bumpVersion(entry.versionCheck.latestVersion, bump);
1230
+ }
1231
+ catch {
1232
+ return { ...state, errorMessage: `Failed to compute bumped version for ${entry.label}.`, phase: 'error' };
1233
+ }
1234
+ return advanceToPlan({ ...state, entries: replaceEntry(state.entries, index, withVersion(entry, version)) });
1235
+ }
1236
+ /** A raw publish's step after its description: the tags prompt, unless --tags answered it. */
1237
+ function metadataAfterDescription(state) {
1238
+ return state.flags.tagsFlag === undefined ? 'tags' : 'building-staging';
1239
+ }
1240
+ /**
1241
+ * Phase to enter once the tags/tools metadata prompts are resolved: org-scoped
1242
+ * targets get the reviewer prompt, everything else proceeds to the registry check.
1243
+ */
1244
+ function phaseAfterMetadata(target) {
1245
+ return target && isOrgScopedTarget(target) ? 'prompting-reviewer' : 'checking-registry';
1246
+ }
1247
+ function replaceEntry(entries, index, entry) {
1248
+ return entries.map((current, i) => (i === index ? entry : current));
1249
+ }
1250
+ /** The entry published at `version`; a successful publish writes it to the entry's manifest. */
1251
+ function withVersion(entry, version) {
1252
+ return {
1253
+ ...entry,
1254
+ resolvedVersion: version,
1255
+ target: { ...entry.target, data: { ...entry.target.data, version } },
1256
+ };
1257
+ }
802
1258
  //# sourceMappingURL=publish-flow.js.map