@detergent-software/atk 8.9.0-dev.2 → 8.9.0-dev.21

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 (239) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +13 -3
  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.js +1 -1
  28. package/build/commands/index.js.map +1 -1
  29. package/build/commands/info.d.ts.map +1 -1
  30. package/build/commands/info.js +50 -33
  31. package/build/commands/info.js.map +1 -1
  32. package/build/commands/init.d.ts.map +1 -1
  33. package/build/commands/init.js +4 -2
  34. package/build/commands/init.js.map +1 -1
  35. package/build/commands/install.d.ts.map +1 -1
  36. package/build/commands/install.js +28 -7
  37. package/build/commands/install.js.map +1 -1
  38. package/build/commands/list.js +2 -2
  39. package/build/commands/list.js.map +1 -1
  40. package/build/commands/login.d.ts.map +1 -1
  41. package/build/commands/login.js +12 -2
  42. package/build/commands/login.js.map +1 -1
  43. package/build/commands/logout.d.ts.map +1 -1
  44. package/build/commands/logout.js +9 -1
  45. package/build/commands/logout.js.map +1 -1
  46. package/build/commands/outdated.js +1 -1
  47. package/build/commands/outdated.js.map +1 -1
  48. package/build/commands/publish.d.ts +1 -1
  49. package/build/commands/publish.d.ts.map +1 -1
  50. package/build/commands/publish.js +32 -36
  51. package/build/commands/publish.js.map +1 -1
  52. package/build/commands/release-notes.d.ts.map +1 -1
  53. package/build/commands/release-notes.js +4 -0
  54. package/build/commands/release-notes.js.map +1 -1
  55. package/build/commands/schema.d.ts +3 -3
  56. package/build/commands/search.js +1 -1
  57. package/build/commands/search.js.map +1 -1
  58. package/build/commands/sync.d.ts +1 -1
  59. package/build/commands/sync.d.ts.map +1 -1
  60. package/build/commands/sync.js +27 -12
  61. package/build/commands/sync.js.map +1 -1
  62. package/build/commands/update.d.ts.map +1 -1
  63. package/build/commands/update.js +58 -9
  64. package/build/commands/update.js.map +1 -1
  65. package/build/commands/whoami.d.ts.map +1 -1
  66. package/build/commands/whoami.js +16 -4
  67. package/build/commands/whoami.js.map +1 -1
  68. package/build/commands/why.js +3 -4
  69. package/build/commands/why.js.map +1 -1
  70. package/build/components/Header.d.ts +2 -3
  71. package/build/components/Header.d.ts.map +1 -1
  72. package/build/components/Header.js +2 -9
  73. package/build/components/Header.js.map +1 -1
  74. package/build/components/LocalEditsHint.d.ts +16 -0
  75. package/build/components/LocalEditsHint.d.ts.map +1 -0
  76. package/build/components/LocalEditsHint.js +18 -0
  77. package/build/components/LocalEditsHint.js.map +1 -0
  78. package/build/components/SyncAllInSync.d.ts +5 -1
  79. package/build/components/SyncAllInSync.d.ts.map +1 -1
  80. package/build/components/SyncAllInSync.js +2 -2
  81. package/build/components/SyncAllInSync.js.map +1 -1
  82. package/build/components/header-art.d.ts +7 -0
  83. package/build/components/header-art.d.ts.map +1 -0
  84. package/build/components/header-art.js +14 -0
  85. package/build/components/header-art.js.map +1 -0
  86. package/build/components/index.d.ts +1 -0
  87. package/build/components/index.d.ts.map +1 -1
  88. package/build/components/index.js +1 -0
  89. package/build/components/index.js.map +1 -1
  90. package/build/hooks/useListActions.d.ts.map +1 -1
  91. package/build/hooks/useListActions.js +4 -0
  92. package/build/hooks/useListActions.js.map +1 -1
  93. package/build/hooks/usePublishState.d.ts +38 -207
  94. package/build/hooks/usePublishState.d.ts.map +1 -1
  95. package/build/hooks/usePublishState.js +83 -1501
  96. package/build/hooks/usePublishState.js.map +1 -1
  97. package/build/lib/adapter.js +23 -3
  98. package/build/lib/adapter.js.map +1 -1
  99. package/build/lib/api-client.d.ts +9 -5
  100. package/build/lib/api-client.d.ts.map +1 -1
  101. package/build/lib/api-client.js +35 -8
  102. package/build/lib/api-client.js.map +1 -1
  103. package/build/lib/atk-api/generated/types.gen.d.ts +4 -4
  104. package/build/lib/auth.d.ts +12 -2
  105. package/build/lib/auth.d.ts.map +1 -1
  106. package/build/lib/auth.js +38 -3
  107. package/build/lib/auth.js.map +1 -1
  108. package/build/lib/checksum.d.ts +40 -14
  109. package/build/lib/checksum.d.ts.map +1 -1
  110. package/build/lib/checksum.js +110 -57
  111. package/build/lib/checksum.js.map +1 -1
  112. package/build/lib/config.d.ts.map +1 -1
  113. package/build/lib/config.js +28 -5
  114. package/build/lib/config.js.map +1 -1
  115. package/build/lib/diagnostics.d.ts.map +1 -1
  116. package/build/lib/diagnostics.js +10 -7
  117. package/build/lib/diagnostics.js.map +1 -1
  118. package/build/lib/diff-command.js +1 -1
  119. package/build/lib/diff-command.js.map +1 -1
  120. package/build/lib/errors.d.ts +4 -1
  121. package/build/lib/errors.d.ts.map +1 -1
  122. package/build/lib/errors.js.map +1 -1
  123. package/build/lib/format-age.d.ts +3 -0
  124. package/build/lib/format-age.d.ts.map +1 -0
  125. package/build/lib/format-age.js +18 -0
  126. package/build/lib/format-age.js.map +1 -0
  127. package/build/lib/installer.d.ts +2 -7
  128. package/build/lib/installer.d.ts.map +1 -1
  129. package/build/lib/installer.js +30 -5
  130. package/build/lib/installer.js.map +1 -1
  131. package/build/lib/local-edits.d.ts +72 -0
  132. package/build/lib/local-edits.d.ts.map +1 -0
  133. package/build/lib/local-edits.js +145 -0
  134. package/build/lib/local-edits.js.map +1 -0
  135. package/build/lib/lockfile.d.ts +12 -8
  136. package/build/lib/lockfile.d.ts.map +1 -1
  137. package/build/lib/lockfile.js +193 -52
  138. package/build/lib/lockfile.js.map +1 -1
  139. package/build/lib/mover.d.ts.map +1 -1
  140. package/build/lib/mover.js +8 -12
  141. package/build/lib/mover.js.map +1 -1
  142. package/build/lib/offline.d.ts +19 -0
  143. package/build/lib/offline.d.ts.map +1 -0
  144. package/build/lib/offline.js +26 -0
  145. package/build/lib/offline.js.map +1 -0
  146. package/build/lib/owned-paths.d.ts +4 -2
  147. package/build/lib/owned-paths.d.ts.map +1 -1
  148. package/build/lib/owned-paths.js +19 -6
  149. package/build/lib/owned-paths.js.map +1 -1
  150. package/build/lib/pastel/commander-specs.d.ts +7 -0
  151. package/build/lib/pastel/commander-specs.d.ts.map +1 -0
  152. package/build/lib/pastel/commander-specs.js +50 -0
  153. package/build/lib/pastel/commander-specs.js.map +1 -0
  154. package/build/lib/pastel/generate-arguments.d.ts +3 -3
  155. package/build/lib/pastel/generate-arguments.d.ts.map +1 -1
  156. package/build/lib/pastel/generate-arguments.js +25 -22
  157. package/build/lib/pastel/generate-arguments.js.map +1 -1
  158. package/build/lib/pastel/generate-command.d.ts +7 -5
  159. package/build/lib/pastel/generate-command.d.ts.map +1 -1
  160. package/build/lib/pastel/generate-command.js +32 -67
  161. package/build/lib/pastel/generate-command.js.map +1 -1
  162. package/build/lib/pastel/generate-commands.d.ts +3 -5
  163. package/build/lib/pastel/generate-commands.d.ts.map +1 -1
  164. package/build/lib/pastel/generate-commands.js +6 -10
  165. package/build/lib/pastel/generate-commands.js.map +1 -1
  166. package/build/lib/pastel/generate-options.d.ts +3 -3
  167. package/build/lib/pastel/generate-options.d.ts.map +1 -1
  168. package/build/lib/pastel/generate-options.js +16 -13
  169. package/build/lib/pastel/generate-options.js.map +1 -1
  170. package/build/lib/pastel/index.d.ts +26 -4
  171. package/build/lib/pastel/index.d.ts.map +1 -1
  172. package/build/lib/pastel/index.js +104 -18
  173. package/build/lib/pastel/index.js.map +1 -1
  174. package/build/lib/pastel/internal-types.d.ts +39 -8
  175. package/build/lib/pastel/internal-types.d.ts.map +1 -1
  176. package/build/lib/pastel/manifest.d.ts +8 -0
  177. package/build/lib/pastel/manifest.d.ts.map +1 -0
  178. package/build/lib/pastel/manifest.js +7 -0
  179. package/build/lib/pastel/manifest.js.map +1 -0
  180. package/build/lib/pastel/read-commands.d.ts +7 -2
  181. package/build/lib/pastel/read-commands.d.ts.map +1 -1
  182. package/build/lib/pastel/read-commands.js +51 -30
  183. package/build/lib/pastel/read-commands.js.map +1 -1
  184. package/build/lib/pastel/render-command.d.ts +13 -0
  185. package/build/lib/pastel/render-command.d.ts.map +1 -0
  186. package/build/lib/pastel/render-command.js +56 -0
  187. package/build/lib/pastel/render-command.js.map +1 -0
  188. package/build/lib/publish-flow.d.ts +225 -0
  189. package/build/lib/publish-flow.d.ts.map +1 -0
  190. package/build/lib/publish-flow.js +1242 -0
  191. package/build/lib/publish-flow.js.map +1 -0
  192. package/build/lib/publish-run.d.ts +55 -0
  193. package/build/lib/publish-run.d.ts.map +1 -0
  194. package/build/lib/publish-run.js +164 -0
  195. package/build/lib/publish-run.js.map +1 -0
  196. package/build/lib/publisher.d.ts +11 -16
  197. package/build/lib/publisher.d.ts.map +1 -1
  198. package/build/lib/publisher.js +25 -42
  199. package/build/lib/publisher.js.map +1 -1
  200. package/build/lib/registry.d.ts +15 -1
  201. package/build/lib/registry.d.ts.map +1 -1
  202. package/build/lib/registry.js +108 -24
  203. package/build/lib/registry.js.map +1 -1
  204. package/build/lib/root-path.d.ts +11 -0
  205. package/build/lib/root-path.d.ts.map +1 -1
  206. package/build/lib/root-path.js +15 -1
  207. package/build/lib/root-path.js.map +1 -1
  208. package/build/lib/schemas/config.js +1 -1
  209. package/build/lib/schemas/config.js.map +1 -1
  210. package/build/lib/schemas/lockfile.d.ts +156 -23
  211. package/build/lib/schemas/lockfile.d.ts.map +1 -1
  212. package/build/lib/schemas/lockfile.js +100 -79
  213. package/build/lib/schemas/lockfile.js.map +1 -1
  214. package/build/lib/staging.d.ts +15 -0
  215. package/build/lib/staging.d.ts.map +1 -0
  216. package/build/lib/staging.js +33 -0
  217. package/build/lib/staging.js.map +1 -0
  218. package/build/lib/sync.d.ts +22 -6
  219. package/build/lib/sync.d.ts.map +1 -1
  220. package/build/lib/sync.js +59 -7
  221. package/build/lib/sync.js.map +1 -1
  222. package/build/lib/uninstaller.js +2 -2
  223. package/build/lib/uninstaller.js.map +1 -1
  224. package/build/lib/update-check.d.ts.map +1 -1
  225. package/build/lib/update-check.js +5 -1
  226. package/build/lib/update-check.js.map +1 -1
  227. package/build/lib/updater.d.ts +31 -1
  228. package/build/lib/updater.d.ts.map +1 -1
  229. package/build/lib/updater.js +143 -4
  230. package/build/lib/updater.js.map +1 -1
  231. package/build/lib/usage-error.d.ts +20 -0
  232. package/build/lib/usage-error.d.ts.map +1 -0
  233. package/build/lib/usage-error.js +41 -0
  234. package/build/lib/usage-error.js.map +1 -0
  235. package/build/lib/write-file.d.ts +5 -0
  236. package/build/lib/write-file.d.ts.map +1 -1
  237. package/build/lib/write-file.js +10 -7
  238. package/build/lib/write-file.js.map +1 -1
  239. package/package.json +81 -81
@@ -1,1523 +1,105 @@
1
- import { cp, mkdir, rm, writeFile } from 'node:fs/promises';
2
- import { tmpdir } from 'node:os';
3
- import { dirname, join, resolve } from 'node:path';
4
- import { useEffect, useReducer, useRef } from 'react';
5
- import { loadAllAdapters, resolveInstalledPaths } from '../lib/adapter.js';
6
- import { isApiError } from '../lib/api-client.js';
7
- import { getCredential } from '../lib/auth.js';
8
- import { hashFile } from '../lib/checksum.js';
9
- import { errorGuidance, errorMessage } from '../lib/errors.js';
10
- import { detectCompatibleTools, getSignedInUserName } from '../lib/init.js';
11
- import { addAssetToLockfile, findInstalledAsset, readLockfile, withLockfileLock, writeLockfile, } from '../lib/lockfile.js';
12
- import { diffManifests, fetchExistingManifest } from '../lib/manifest-diff.js';
13
- import { parseOrgFromName } from '../lib/org.js';
14
- import { findProjectRoot } from '../lib/paths.js';
15
- import { buildPublishPlan, buildStagingArea, bumpVersion, checkRegistryVersion, detectAssetFromPath, detectPublishType, executePublish, findSiblingDependencies, isOrgScopedTarget, loadPublishBatch, loadPublishEntries, mapPublishError, missingDependencies, resolvePlanDependencies, unknownToolsError, updateManifestField, updateManifestVersion, validatePublishTarget, } from '../lib/publisher.js';
16
- import { fetchAssetManifest } from '../lib/registry.js';
17
- import { clearCache, fetchRegistry, findAsset, findBundle } from '../lib/registry.js';
18
- import { parseReviewersInput, validateGithubUsername } from '../lib/reviewers.js';
19
- import { resolveTools } from '../lib/tool-resolver.js';
20
- // ---------------------------------------------------------------------------
21
- // Error helpers
22
- // ---------------------------------------------------------------------------
23
- export function createInitialState(flags) {
24
- // Non-interactive when --yes, --dry-run, --json, or stdin is not a TTY (raw mode
25
- // unsupported). Defaults isTTY to true when the flag is omitted (call site responsible
26
- // for threading useStdin().isRawModeSupported in). --dry-run and --json force
27
- // non-interactive regardless of TTY — JSON output cannot interleave with prompts.
28
- const isTTY = flags.isTTY ?? true;
29
- const nonInteractive = Boolean(flags.yes) || flags.dryRun || Boolean(flags.json) || !isTTY;
30
- const flagError = batchFlagError(flags);
31
- return {
32
- authorValue: '',
33
- bumpSelection: '',
34
- confirmValue: '',
35
- descriptionValue: '',
36
- entries: [],
37
- errorMessage: flagError ?? '',
38
- flags,
39
- isRawPublish: false,
40
- isUpdate: false,
41
- manifestDiff: undefined,
42
- metadataSubPhase: 'description',
43
- missingDependencies: [],
44
- nonInteractive,
45
- orgValue: '',
46
- phase: flagError
47
- ? 'error'
48
- : flags.update
49
- ? 'resolving-update'
50
- : flags.fromInstalled
51
- ? 'resolving-installed'
52
- : 'detecting',
53
- plan: undefined,
54
- progressMessage: '',
55
- promptTagsValue: '',
56
- publishConfirmValue: '',
57
- publishResult: undefined,
58
- rawDetection: null,
59
- resolvedPath: flags.update ? '' : flags.fromInstalled ? '' : resolve(flags.rawPath),
60
- resolvedVersion: '',
61
- reviewerValue: '',
62
- selectedPromptTools: [],
63
- suggestedEntries: [],
64
- tagsValue: '',
65
- target: undefined,
66
- token: undefined,
67
- toolAdapters: [],
68
- toolCursorIndex: 0,
69
- toolsDetected: [],
70
- validation: undefined,
71
- versionCheck: undefined,
72
- withConfirmValue: '',
73
- };
74
- }
75
- // ---------------------------------------------------------------------------
76
- // Initial state factory
77
- // ---------------------------------------------------------------------------
78
- /** Several folders published together (`atk publish a b c`) in one pull request; one folder named twice is one. */
79
- export function isBatch(flags) {
80
- return new Set(flags.paths?.map((path) => resolve(path))).size > 1;
81
- }
82
- export function publishReducer(state, action) {
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { createInitialState } from '../lib/publish-flow.js';
3
+ import { promptValue, runPublish } from '../lib/publish-run.js';
4
+ /** Move the tools prompt's cursor over its `count` rows, wrapping at either end, or tick or untick a tool. */
5
+ export function toolPickerReducer(picker, action) {
83
6
  switch (action.type) {
84
- case 'BUMP_DONE': {
85
- const bumped = {
86
- ...state,
87
- errorMessage: action.message ? action.message : state.errorMessage,
88
- resolvedVersion: action.version,
89
- target: action.updatedTarget,
90
- };
91
- return action.message ? { ...bumped, phase: 'error' } : advanceToPlan(bumped);
92
- }
93
- case 'CONFIRM_PROMPT_TOOLS': {
94
- if (state.selectedPromptTools.length === 0)
95
- return state;
96
- const updatedToolsTarget = state.target
97
- ? {
98
- ...state.target,
99
- data: { ...state.target.data, tools: state.selectedPromptTools },
100
- }
101
- : undefined;
102
- return {
103
- ...state,
104
- phase: phaseAfterMetadata(updatedToolsTarget),
105
- target: updatedToolsTarget,
106
- };
107
- }
108
- case 'DETECT_DONE':
109
- if (action.target.type === 'bundle' && state.entries.length > 0) {
110
- return {
111
- ...state,
112
- errorMessage: 'A bundle is published on its own. Remove --with, and publish its new assets first.',
113
- phase: 'error',
114
- target: action.target,
115
- };
116
- }
117
- return {
118
- ...state,
119
- phase: 'validating',
120
- target: action.target,
121
- };
122
- case 'ENTRIES_CHECKED':
123
- return advanceToPlan({ ...state, entries: action.entries });
124
- case 'ENTRIES_LOADED':
125
- return { ...state, entries: action.entries };
126
- case 'ERROR':
127
- return {
128
- ...state,
129
- errorCode: action.code,
130
- errorCommand: action.command,
131
- errorHint: action.hint,
132
- errorMessage: action.message,
133
- phase: 'error',
134
- };
135
- case 'INSTALL_DONE':
136
- return { ...state, phase: 'done' };
137
- case 'PLAN_DONE': {
138
- // dry-run always shows the dry-run result; --yes / non-TTY skip the confirm prompt
139
- // and proceed directly to publishing.
140
- let nextPhase;
141
- if (state.flags.dryRun) {
142
- nextPhase = 'dry-run-result';
143
- }
144
- else if (state.nonInteractive) {
145
- nextPhase = 'publishing';
146
- }
147
- else {
148
- nextPhase = 'confirm-publish';
149
- }
150
- return {
151
- ...state,
152
- manifestDiff: action.manifestDiff,
153
- phase: nextPhase,
154
- plan: action.plan,
155
- };
156
- }
157
- case 'PUBLISH_DONE': {
158
- const nextPhase = state.isRawPublish ? 'installing' : 'done';
159
- return {
160
- ...state,
161
- phase: nextPhase,
162
- publishResult: action.result,
163
- };
164
- }
165
- case 'PUBLISH_PROGRESS':
166
- return { ...state, progressMessage: action.message };
167
- case 'RAW_DETECT_DONE':
168
- return {
169
- ...state,
170
- authorValue: action.authorValue,
171
- isRawPublish: true,
172
- orgValue: action.orgValue,
173
- phase: 'gathering-metadata',
174
- rawDetection: action.rawDetection,
175
- toolsDetected: action.toolsDetected,
176
- };
177
- case 'RAW_DETECT_DONE_NON_INTERACTIVE':
178
- return {
179
- ...state,
180
- isRawPublish: true,
181
- phase: 'validating',
182
- rawDetection: action.rawDetection,
183
- target: action.target,
184
- };
185
- case 'REGISTRY_CHECK_DONE': {
186
- const { isUpdate, resolvedVersion, token, versionCheck } = action;
187
- let nextPhase;
188
- let errorMessage = state.errorMessage;
189
- let bumpedVersion = resolvedVersion;
190
- let bumpedTarget = state.target;
191
- if (versionCheck.status === 'new-asset') {
192
- nextPhase = 'building-plan';
193
- }
194
- else if (versionCheck.status === 'version-already-bumped') {
195
- // --yes / non-TTY implicitly confirm the update
196
- nextPhase = state.nonInteractive ? 'building-plan' : 'confirm-update';
197
- }
198
- else {
199
- // needs-bump
200
- if (state.nonInteractive) {
201
- if (state.flags.bump && state.target) {
202
- // Auto-bump using the flag value; a successful publish writes it to the manifest
203
- try {
204
- bumpedVersion = bumpVersion(versionCheck.latestVersion, state.flags.bump);
205
- bumpedTarget = {
206
- ...state.target,
207
- data: { ...state.target.data, version: bumpedVersion },
208
- };
209
- nextPhase = 'building-plan';
210
- }
211
- catch {
212
- errorMessage = 'Failed to compute bumped version.';
213
- nextPhase = 'error';
214
- }
215
- }
216
- else if (isBatch(state.flags)) {
217
- // advanceToPlan lists every asset of the batch that needs --bump, once the entries are checked.
218
- nextPhase = 'building-plan';
219
- }
220
- else {
221
- errorMessage = `--bump <patch|minor|major> required: registry has v${versionCheck.latestVersion} but manifest is not newer`;
222
- nextPhase = 'error';
223
- }
224
- }
225
- else {
226
- nextPhase = 'bump-prompt';
227
- }
228
- }
229
- const checked = {
230
- ...state,
231
- errorMessage,
232
- isUpdate,
233
- phase: nextPhase,
234
- resolvedVersion: bumpedVersion,
235
- target: bumpedTarget,
236
- token,
237
- versionCheck,
238
- };
239
- return nextPhase === 'building-plan' ? advanceToPlan(checked) : checked;
240
- }
241
- case 'RESOLVE_TOOLS_NONINTERACTIVE': {
242
- const updatedTarget = state.target
243
- ? {
244
- ...state.target,
245
- data: { ...state.target.data, tools: action.tools },
246
- }
247
- : undefined;
248
- return {
249
- ...state,
250
- phase: 'checking-registry',
251
- target: updatedTarget,
252
- };
253
- }
254
- case 'RESOLVED_INSTALLED':
255
- return {
256
- ...state,
257
- phase: 'detecting',
258
- resolvedPath: action.resolvedPath,
259
- };
260
- case 'RESOLVED_UPDATE':
261
- return {
262
- ...state,
263
- phase: 'detecting',
264
- resolvedPath: action.resolvedPath,
265
- };
266
- case 'SET_BUMP_SELECTION':
267
- return { ...state, bumpSelection: action.value };
268
- case 'SET_CONFIRM_VALUE':
269
- return { ...state, confirmValue: action.value };
270
- case 'SET_DESCRIPTION':
271
- return { ...state, descriptionValue: action.value };
272
- case 'SET_PROMPT_TAGS':
273
- return { ...state, promptTagsValue: action.value };
274
- case 'SET_PUBLISH_CONFIRM_VALUE':
275
- return { ...state, publishConfirmValue: action.value };
276
- case 'SET_REVIEWER':
277
- return { ...state, reviewerValue: action.value };
278
- case 'SET_TAGS':
279
- return { ...state, tagsValue: action.value };
280
- case 'SET_WITH_CONFIRM_VALUE':
281
- return { ...state, withConfirmValue: action.value };
282
- case 'STAGING_DONE':
283
- return {
284
- ...state,
285
- phase: 'validating',
286
- target: action.target,
287
- };
288
- case 'SUBMIT_BUMP': {
289
- const trimmed = state.bumpSelection.trim().toLowerCase();
290
- if (trimmed !== 'patch' && trimmed !== 'minor' && trimmed !== 'major') {
291
- return state; // Ignore invalid input
292
- }
293
- // The prompt is for an entry: bump it, then on to the next pending bump or the plan.
294
- if (state.bumpEntry !== undefined) {
295
- const entry = state.entries[state.bumpEntry];
296
- if (entry?.versionCheck?.status !== 'needs-bump')
297
- return state;
298
- return bumpEntryAt({ ...state, bumpSelection: '' }, state.bumpEntry, trimmed);
299
- }
300
- if (!state.target || !state.versionCheck || state.versionCheck.status !== 'needs-bump') {
301
- return state;
302
- }
303
- // Compute the bumped version in the reducer (pure, synchronous)
304
- const bumpType = trimmed;
305
- try {
306
- const newVersion = bumpVersion(state.versionCheck.latestVersion, bumpType);
307
- // The bump is only on the target until the publish succeeds, which writes it to
308
- // the manifest; entries still to check or bump come first.
309
- return advanceToPlan({
310
- ...state,
311
- bumpSelection: '',
312
- resolvedVersion: newVersion,
313
- target: { ...state.target, data: { ...state.target.data, version: newVersion } },
314
- });
315
- }
316
- catch {
317
- return {
318
- ...state,
319
- errorMessage: 'Failed to compute bumped version.',
320
- phase: 'error',
321
- };
322
- }
323
- }
324
- case 'SUBMIT_CONFIRM': {
325
- const trimmed = state.confirmValue.trim().toLowerCase();
326
- if (trimmed === 'y' || trimmed === 'yes') {
327
- return advanceToPlan(state);
328
- }
329
- else if (trimmed === 'n' || trimmed === 'no') {
330
- return { ...state, errorMessage: 'Publish cancelled.', phase: 'error' };
331
- }
332
- // Otherwise ignore (wait for valid input)
333
- return state;
334
- }
335
- case 'SUBMIT_DESCRIPTION': {
336
- const trimmed = state.descriptionValue.trim();
337
- if (!trimmed)
338
- return state; // Don't allow empty description
339
- return {
340
- ...state,
341
- descriptionValue: trimmed,
342
- metadataSubPhase: 'tags',
343
- };
344
- }
345
- case 'SUBMIT_PROMPT_TAGS': {
346
- // Parse comma-separated tags from the prompt value
347
- const parsedTags = state.promptTagsValue
348
- .split(',')
349
- .map((t) => t.trim())
350
- .filter(Boolean);
351
- // Update the target's data with the new tags
352
- const updatedTagsTarget = state.target
353
- ? {
354
- ...state.target,
355
- data: { ...state.target.data, tags: parsedTags.length > 0 ? parsedTags : undefined },
356
- }
357
- : undefined;
358
- // Check if tools are also missing (only for assets)
359
- const needsToolsPrompt = updatedTagsTarget?.type === 'asset' &&
360
- (!('tools' in updatedTagsTarget.data) ||
361
- !Array.isArray(updatedTagsTarget.data.tools) ||
362
- (updatedTagsTarget.data.tools?.length ?? 0) === 0);
363
- const nextTagsPhase = needsToolsPrompt ? 'loading-tools' : phaseAfterMetadata(updatedTagsTarget);
364
- return {
365
- ...state,
366
- phase: nextTagsPhase,
367
- target: updatedTagsTarget,
368
- };
369
- }
370
- case 'SUBMIT_PUBLISH_CONFIRM': {
371
- const trimmed = state.publishConfirmValue.trim().toLowerCase();
372
- if (trimmed === 'y' || trimmed === 'yes') {
373
- return { ...state, phase: 'publishing' };
374
- }
375
- else if (trimmed === 'n' || trimmed === 'no') {
376
- return { ...state, errorMessage: 'Publish cancelled.', phase: 'error' };
377
- }
378
- // Otherwise ignore (wait for valid input)
379
- return state;
380
- }
381
- case 'SUBMIT_REVIEWER':
382
- return { ...state, phase: 'checking-registry' };
383
- case 'SUBMIT_TAGS':
384
- return {
385
- ...state,
386
- metadataSubPhase: 'building-staging',
387
- };
388
- case 'SUBMIT_WITH_CONFIRM': {
389
- const trimmed = state.withConfirmValue.trim().toLowerCase();
390
- if (trimmed === 'y' || trimmed === 'yes') {
391
- return advanceToPlan({
392
- ...state,
393
- entries: [...state.entries, ...state.suggestedEntries],
394
- missingDependencies: [],
395
- pendingError: undefined,
396
- suggestedEntries: [],
397
- withConfirmValue: '',
398
- });
399
- }
400
- if ((trimmed === 'n' || trimmed === 'no') && state.pendingError) {
401
- return publishReducer({ ...state, suggestedEntries: [], withConfirmValue: '' }, state.pendingError);
402
- }
403
- return state;
404
- }
405
- case 'SUGGEST_ENTRIES':
406
- return {
407
- ...state,
408
- missingDependencies: action.missing,
409
- pendingError: action.error,
410
- phase: 'confirm-with',
411
- suggestedEntries: action.suggested,
412
- withConfirmValue: '',
413
- };
414
7
  case 'TOGGLE_PROMPT_TOOL': {
415
- const toolSet = new Set(state.selectedPromptTools);
8
+ const toolSet = new Set(picker.selected);
416
9
  if (toolSet.has(action.tool)) {
417
10
  toolSet.delete(action.tool);
418
11
  }
419
12
  else {
420
13
  toolSet.add(action.tool);
421
14
  }
422
- return {
423
- ...state,
424
- selectedPromptTools: [...toolSet],
425
- };
426
- }
427
- case 'TOOL_CURSOR_DOWN': {
428
- if (state.toolAdapters.length === 0)
429
- return state;
430
- return {
431
- ...state,
432
- toolCursorIndex: state.toolCursorIndex >= state.toolAdapters.length - 1 ? 0 : state.toolCursorIndex + 1,
433
- };
434
- }
435
- case 'TOOL_CURSOR_UP': {
436
- if (state.toolAdapters.length === 0)
437
- return state;
438
- return {
439
- ...state,
440
- toolCursorIndex: state.toolCursorIndex <= 0 ? state.toolAdapters.length - 1 : state.toolCursorIndex - 1,
441
- };
15
+ return { ...picker, selected: [...toolSet] };
442
16
  }
443
- case 'TOOLS_LOADED':
444
- return {
445
- ...state,
446
- phase: 'prompting-tools',
447
- toolAdapters: action.adapters,
448
- toolCursorIndex: 0,
449
- };
450
- case 'VALIDATE_DONE': {
451
- if (!action.validation.valid) {
452
- return {
453
- ...state,
454
- errorMessage: `Validation failed:\n${action.validation.errors.map((e) => ` - ${e}`).join('\n')}`,
455
- phase: 'error',
456
- validation: action.validation,
457
- };
458
- }
459
- // Seed the reviewers field: the --reviewer flag wins over the project's
460
- // lockfile-configured default (org.ts's CLI-flag > lockfile priority chain).
461
- const reviewerValue = state.flags.reviewerFlag ?? (action.lockfileOrgReviewers ?? []).join(', ');
462
- // Determine next phase based on missing tags/tools. A batch asks for neither: as for
463
- // a --with entry, a missing one is only a warning.
464
- const batch = isBatch(state.flags);
465
- const manifest = state.target?.data;
466
- const hasTags = batch || (manifest && 'tags' in manifest && Array.isArray(manifest.tags) && manifest.tags.length > 0);
467
- const hasTools = batch ||
468
- // Only assets have tools; bundles skip the tools prompt
469
- state.target?.type !== 'asset' ||
470
- (manifest && 'tools' in manifest && Array.isArray(manifest.tools) && manifest.tools.length > 0);
471
- const isOrgScoped = batch
472
- ? [state.target, ...state.entries.map((entry) => entry.target)].some((t) => t && isOrgScopedTarget(t))
473
- : Boolean(state.target && isOrgScopedTarget(state.target));
474
- // Reviewers only apply to org-scoped assets (non-org PRs always go to the registry
475
- // maintainer) — fail fast rather than silently ignoring the flag.
476
- if (!isOrgScoped && state.flags.reviewerFlag && parseReviewersInput(state.flags.reviewerFlag).length > 0) {
477
- return {
478
- ...state,
479
- errorMessage: 'The --reviewer flag only applies to org-scoped assets. Remove the flag to publish this asset.',
480
- phase: 'error',
481
- validation: action.validation,
482
- };
483
- }
484
- // Non-interactive path: resolve tags from --tags flag (or empty) right here, then
485
- // either route to loading-tools (effect resolves tools async) or directly to
486
- // checking-registry. Manifest tags always win when present. The reviewer prompt
487
- // never applies here — non-interactive mode always skips it.
488
- if (state.nonInteractive) {
489
- let nextTarget = state.target;
490
- if (!hasTags && nextTarget) {
491
- const flagTags = state.flags.tagsFlag
492
- ? state.flags.tagsFlag
493
- .split(',')
494
- .map((t) => t.trim())
495
- .filter(Boolean)
496
- : [];
497
- nextTarget = {
498
- ...nextTarget,
499
- data: { ...nextTarget.data, tags: flagTags.length > 0 ? flagTags : undefined },
500
- };
501
- }
502
- let nextPhase;
503
- if (!hasTools) {
504
- // The loading-tools effect detects whether we're non-interactive and
505
- // either resolves tools from the manifest/flag/inference or errors out.
506
- nextPhase = 'loading-tools';
507
- }
508
- else {
509
- nextPhase = 'checking-registry';
510
- }
511
- return {
512
- ...state,
513
- phase: nextPhase,
514
- reviewerValue,
515
- target: nextTarget,
516
- validation: action.validation,
517
- };
518
- }
519
- let nextPhase;
520
- if (!hasTags) {
521
- nextPhase = 'prompting-tags';
522
- }
523
- else if (!hasTools) {
524
- nextPhase = 'loading-tools';
525
- }
526
- else {
527
- nextPhase = isOrgScoped ? 'prompting-reviewer' : 'checking-registry';
528
- }
529
- return {
530
- ...state,
531
- phase: nextPhase,
532
- reviewerValue,
533
- validation: action.validation,
534
- };
535
- }
536
- default:
537
- return state;
17
+ case 'TOOL_CURSOR_DOWN':
18
+ if (action.count === 0)
19
+ return picker;
20
+ return { ...picker, cursor: picker.cursor >= action.count - 1 ? 0 : picker.cursor + 1 };
21
+ case 'TOOL_CURSOR_UP':
22
+ if (action.count === 0)
23
+ return picker;
24
+ return { ...picker, cursor: picker.cursor <= 0 ? action.count - 1 : picker.cursor - 1 };
538
25
  }
539
26
  }
27
+ /**
28
+ * The publish flow for the command's UI. `runPublish` (lib/publish-run.ts) runs it once
29
+ * and this mirrors each state it moves to. The hook is its prompter: a prompt waits on
30
+ * `input`, which shows the state's own value for that prompt (an answer the reducer
31
+ * didn't take stays) until something is typed, or, for tools, on `picker`.
32
+ */
540
33
  export function usePublishState(flags) {
541
- const [state, dispatch] = useReducer(publishReducer, undefined, () => createInitialState(flags));
542
- // Temp directory ref for staging / --from-installed cleanup
543
- const tempDirRef = useRef(null);
544
- // --- Phase: resolving-installed ---
545
- useEffect(() => {
546
- if (state.phase !== 'resolving-installed')
547
- return;
548
- let cancelled = false;
549
- const resolveInstalled = async () => {
550
- const assetName = state.flags.rawPath;
551
- const { name, org } = parseOrgFromName(assetName);
552
- const projectRoot = findProjectRoot(state.flags.projectRoot);
553
- const tools = await resolveTools(state.flags.tool);
554
- const adapter = tools[0].adapter;
555
- const lockfile = await readLockfile(projectRoot);
556
- const installed = findInstalledAsset(lockfile, name, undefined, org);
557
- if (!installed) {
558
- if (!cancelled) {
559
- dispatch({ message: `Asset '${assetName}' is not installed.`, type: 'ERROR' });
560
- }
561
- return;
562
- }
563
- // Create temp directory and copy installed files
564
- const tempDir = await mkdir(join(tmpdir(), 'atk-publish-'), { recursive: true })
565
- .then(() => join(tmpdir(), `atk-publish-${Date.now()}-${Math.random().toString(36).slice(2)}`))
566
- .then(async (dir) => {
567
- await mkdir(dir, { recursive: true });
568
- return dir;
569
- });
570
- if (cancelled) {
571
- await rm(tempDir, { force: true, recursive: true });
572
- return;
573
- }
574
- tempDirRef.current = tempDir;
575
- // Resolve installed paths via the adapter
576
- const resolved = resolveInstalledPaths(installed, adapter, projectRoot);
577
- // Copy each installed file preserving sourcePath directory structure
578
- for (const file of resolved.files) {
579
- const sourceAbsolute = join(projectRoot, file.installedPath);
580
- const destPath = join(tempDir, file.sourcePath);
581
- const destDir = dirname(destPath);
582
- await mkdir(destDir, { recursive: true });
583
- await cp(sourceAbsolute, destPath);
584
- }
585
- // Generate manifest.json in the temp dir
586
- const manifest = {
587
- author: 'unknown',
588
- description: `Published from installed asset ${installed.name}`,
589
- entrypoint: installed.files[0]?.sourcePath ?? 'index.md',
590
- name: installed.name,
591
- ...(installed.org ? { org: installed.org } : {}),
592
- type: installed.type,
593
- version: installed.version,
594
- };
595
- await writeFile(join(tempDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
596
- if (!cancelled) {
597
- dispatch({ resolvedPath: tempDir, type: 'RESOLVED_INSTALLED' });
598
- }
599
- };
600
- resolveInstalled().catch((err) => {
601
- if (!cancelled) {
602
- dispatch(toErrorAction(err));
603
- }
604
- });
605
- return () => {
606
- cancelled = true;
607
- };
608
- // Phase-gated effect: only runs when entering 'resolving-installed'
609
- // eslint-disable-next-line react-hooks/exhaustive-deps
610
- }, [state.phase]);
611
- // --- Phase: resolving-update ---
612
- useEffect(() => {
613
- if (state.phase !== 'resolving-update')
614
- return;
615
- let cancelled = false;
616
- const resolveUpdate = async () => {
617
- const assetName = state.flags.rawPath;
618
- const { name, org } = parseOrgFromName(assetName);
619
- const projectRoot = findProjectRoot(state.flags.projectRoot);
620
- const tools = await resolveTools(state.flags.tool);
621
- const adapter = tools[0].adapter;
622
- const lockfile = await readLockfile(projectRoot);
623
- const installed = findInstalledAsset(lockfile, name, undefined, org);
624
- if (!installed) {
625
- if (!cancelled) {
626
- dispatch({ message: `Asset '${assetName}' is not installed.`, type: 'ERROR' });
627
- }
628
- return;
629
- }
630
- // Fetch the registry to find the latest version
631
- const registry = await fetchRegistry({ force: state.flags.refresh });
632
- if (cancelled)
633
- return;
634
- // Look up the asset in the registry: try findAsset first, then findBundle
635
- const resolvedAsset = findAsset(registry, installed.name, installed.type, undefined, installed.org ?? org);
636
- const resolvedBundle = resolvedAsset ? undefined : findBundle(registry, installed.name, installed.org ?? org);
637
- if (!resolvedAsset && !resolvedBundle) {
638
- if (!cancelled) {
639
- dispatch({ message: `Asset '${assetName}' not found in the registry.`, type: 'ERROR' });
640
- }
641
- return;
642
- }
643
- const latestVersion = resolvedAsset ? resolvedAsset.version : resolvedBundle.version;
644
- const assetType = resolvedAsset ? resolvedAsset.asset.type : 'bundle';
645
- // Fetch the authoritative manifest (and optional README) from the registry
646
- const fetched = await fetchAssetManifest({ name: installed.name, org: installed.org ?? org, type: assetType }, latestVersion);
647
- if (cancelled)
648
- return;
649
- // Create temp staging directory
650
- const tempDir = await mkdir(join(tmpdir(), 'atk-publish-'), { recursive: true })
651
- .then(() => join(tmpdir(), `atk-publish-${Date.now()}-${Math.random().toString(36).slice(2)}`))
652
- .then(async (dir) => {
653
- await mkdir(dir, { recursive: true });
654
- return dir;
655
- });
656
- if (cancelled) {
657
- await rm(tempDir, { force: true, recursive: true });
658
- return;
659
- }
660
- tempDirRef.current = tempDir;
661
- // Resolve installed paths and copy the user's locally modified files into staging
662
- const resolved = resolveInstalledPaths(installed, adapter, projectRoot);
663
- for (const file of resolved.files) {
664
- const sourceAbsolute = join(projectRoot, file.installedPath);
665
- const destPath = join(tempDir, file.sourcePath);
666
- const destDir = dirname(destPath);
667
- await mkdir(destDir, { recursive: true });
668
- await cp(sourceAbsolute, destPath);
669
- }
670
- // Write the fetched manifest into the staging directory. It is the manifest as this
671
- // CLI's schema parsed it: a field the schema does not know (from a newer publisher)
672
- // is dropped here and so is missing from the re-published version.
673
- await writeFile(join(tempDir, fetched.manifestFileName), fetched.manifestContent, 'utf-8');
674
- // Write README if present
675
- if (fetched.readmeContent) {
676
- await writeFile(join(tempDir, 'README.md'), fetched.readmeContent, 'utf-8');
677
- }
678
- if (!cancelled) {
679
- dispatch({ resolvedPath: tempDir, type: 'RESOLVED_UPDATE' });
680
- }
681
- };
682
- resolveUpdate().catch((err) => {
683
- if (!cancelled) {
684
- dispatch(toErrorAction(err));
685
- }
686
- });
687
- return () => {
688
- cancelled = true;
689
- };
690
- // Phase-gated effect: only runs when entering 'resolving-update'
691
- // eslint-disable-next-line react-hooks/exhaustive-deps
692
- }, [state.phase]);
693
- // --- Phase: detecting ---
694
- useEffect(() => {
695
- if (state.phase !== 'detecting')
696
- return;
697
- let cancelled = false;
698
- const detect = async () => {
699
- // A batch reads every folder now; any that is missing, a bundle or not an asset folder
700
- // fails here, all of them together, before any prompt.
701
- if (isBatch(state.flags)) {
702
- const { entries, root } = await loadPublishBatch(state.flags.paths ?? [], state.flags.withPaths);
703
- if (cancelled)
704
- return;
705
- dispatch({ entries, type: 'ENTRIES_LOADED' });
706
- dispatch({ target: root.target, type: 'DETECT_DONE' });
707
- return;
708
- }
709
- // A --with folder that is missing, a bundle, the root or a repeat fails here, before any
710
- // prompt; its validation and version check come in checking-entries.
711
- if (state.flags.withPaths?.length) {
712
- const entries = await loadPublishEntries(state.flags.withPaths, state.resolvedPath);
713
- if (cancelled)
714
- return;
715
- dispatch({ entries, type: 'ENTRIES_LOADED' });
716
- }
717
- // First, try manifest-based detection (existing flow)
718
- try {
719
- const detected = await detectPublishType(state.resolvedPath);
720
- if (cancelled)
721
- return;
722
- dispatch({ target: detected, type: 'DETECT_DONE' });
723
- return;
724
- }
725
- catch (manifestError) {
726
- // Only attempt raw detection if the error is specifically about missing manifest/bundle
727
- const manifestMsg = errorMessage(manifestError);
728
- if (!manifestMsg.includes('No manifest.json or bundle.json found')) {
729
- // This is a different error (e.g., invalid JSON in manifest) — surface it directly
730
- if (!cancelled) {
731
- dispatch({ message: manifestMsg, type: 'ERROR' });
732
- }
733
- return;
734
- }
735
- }
736
- // Manifest not found — attempt raw asset detection
737
- const resolvedTools = await resolveTools(state.flags.tool);
738
- const adapter = resolvedTools[0].adapter;
739
- if (cancelled)
740
- return;
741
- const projectRoot = findProjectRoot(state.flags.projectRoot);
742
- const detection = await detectAssetFromPath(state.resolvedPath, adapter, projectRoot);
743
- if (cancelled)
744
- return;
745
- // Pre-fill metadata
746
- const author = (await getSignedInUserName()) ?? '';
747
- if (cancelled)
748
- return;
749
- const lockfile = await readLockfile(projectRoot);
750
- if (cancelled)
751
- return;
752
- const tools = await detectCompatibleTools(detection.assetType);
753
- if (cancelled)
754
- return;
755
- const orgVal = lockfile.org ?? '';
756
- // Non-interactive mode (raw publish — no manifest exists yet, so the only
757
- // sources for description are --description flag or exit-1; tags come from
758
- // --tags or default to empty). Tool detection runs for both modes above.
759
- if (state.nonInteractive) {
760
- if (!state.flags.descriptionFlag) {
761
- if (!cancelled) {
762
- dispatch({
763
- message: 'description required: provide --description or include in manifest.json',
764
- type: 'ERROR',
765
- });
766
- }
767
- return;
768
- }
769
- const tags = state.flags.tagsFlag
770
- ? state.flags.tagsFlag
771
- .split(',')
772
- .map((t) => t.trim())
773
- .filter(Boolean)
774
- : [];
775
- // Tools resolution: --tools flag -> detected (already populated above)
776
- let toolsList;
777
- if (state.flags.toolsFlag) {
778
- toolsList = state.flags.toolsFlag
779
- .split(',')
780
- .map((t) => t.trim())
781
- .filter(Boolean);
782
- }
783
- else if (tools.length > 0) {
784
- toolsList = tools;
785
- }
786
- else {
787
- if (!cancelled) {
788
- dispatch({
789
- message: `--tools <comma-separated> required: no tools detected for type ${detection.assetType}`,
790
- type: 'ERROR',
791
- });
792
- }
793
- return;
794
- }
795
- const stagingDir = await buildStagingArea(detection, {
796
- author,
797
- description: state.flags.descriptionFlag,
798
- ...(lockfile.org ? { org: lockfile.org } : {}),
799
- tags,
800
- tools: toolsList,
801
- });
802
- if (cancelled)
803
- return;
804
- tempDirRef.current = stagingDir;
805
- const detected = await detectPublishType(stagingDir);
806
- if (cancelled)
807
- return;
808
- dispatch({ rawDetection: detection, target: detected, type: 'RAW_DETECT_DONE_NON_INTERACTIVE' });
809
- }
810
- else {
811
- // Interactive mode — gather metadata from the user
812
- dispatch({
813
- authorValue: author,
814
- orgValue: orgVal,
815
- rawDetection: detection,
816
- toolsDetected: tools,
817
- type: 'RAW_DETECT_DONE',
818
- });
819
- }
820
- };
821
- detect().catch((err) => {
822
- if (!cancelled) {
823
- dispatch(toErrorAction(err));
824
- }
825
- });
826
- return () => {
827
- cancelled = true;
828
- };
829
- // Phase-gated effect: only runs when entering 'detecting'
830
- // eslint-disable-next-line react-hooks/exhaustive-deps
831
- }, [state.phase]);
832
- // --- Phase: gathering-metadata / building-staging sub-phase ---
34
+ const [state, setState] = useState(() => createInitialState(flags));
35
+ // What was typed, and for which state: a new state shows its own value, in the same render
36
+ const [edit, setEdit] = useState();
37
+ const [picker, setPicker] = useState({ cursor: 0, selected: [] });
38
+ // Enter and the picker's keys read the latest input, as the reducer read its own fields, not a render's
39
+ const typed = useRef('');
40
+ const ticked = useRef(picker);
41
+ const answer = useRef(undefined);
42
+ const answerTools = useRef(undefined);
833
43
  useEffect(() => {
834
- if (state.phase !== 'gathering-metadata' || state.metadataSubPhase !== 'building-staging')
835
- return;
836
- let cancelled = false;
837
- const build = async () => {
838
- // Parse tags from comma-separated string
839
- const tags = state.tagsValue
840
- .split(',')
841
- .map((t) => t.trim())
842
- .filter(Boolean);
843
- // Build staging area
844
- const stagingDir = await buildStagingArea(state.rawDetection, {
845
- author: state.authorValue,
846
- description: state.descriptionValue,
847
- org: state.orgValue || undefined,
848
- tags,
849
- tools: state.toolsDetected,
850
- });
851
- if (cancelled)
852
- return;
853
- // Store for cleanup
854
- tempDirRef.current = stagingDir;
855
- // Re-detect using the staging area (now has manifest.json)
856
- const detectedTarget = await detectPublishType(stagingDir);
857
- if (cancelled)
858
- return;
859
- dispatch({ target: detectedTarget, type: 'STAGING_DONE' });
860
- };
861
- build().catch((err) => {
862
- if (!cancelled) {
863
- dispatch(toErrorAction(err));
864
- }
865
- });
866
- return () => {
867
- cancelled = true;
868
- };
869
- // Phase-gated effect: only runs when entering building-staging sub-phase
870
- // eslint-disable-next-line react-hooks/exhaustive-deps
871
- }, [state.phase, state.metadataSubPhase]);
872
- // --- Phase: validating ---
873
- useEffect(() => {
874
- if (state.phase !== 'validating')
875
- return;
876
- if (!state.target)
877
- return;
878
- let cancelled = false;
879
- const validate = async () => {
880
- let lockfileOrg;
881
- let lockfileOrgReviewers;
882
- try {
883
- const projectRoot = findProjectRoot(state.flags.projectRoot);
884
- const lockfile = await readLockfile(projectRoot);
885
- lockfileOrg = lockfile.org;
886
- lockfileOrgReviewers = lockfile.orgReviewers;
887
- }
888
- catch {
889
- // No project root / lockfile — treat as no lockfile org / reviewers
890
- }
891
- if (cancelled)
892
- return;
893
- // Bundles need the registry so their asset refs can be resolved during
894
- // validation — an unresolvable ref must fail here, not at install time.
895
- let registry;
896
- if (state.target.type === 'bundle') {
897
- registry = await fetchRegistry({ force: state.flags.refresh });
898
- if (cancelled)
899
- return;
900
- }
901
- const result = await validatePublishTarget(state.target, {
902
- force: state.flags.force,
903
- lockfileOrg,
904
- registry,
905
- });
906
- if (cancelled)
907
- return;
908
- // A batch checks every asset before stopping, so one run lists every folder's problems.
909
- if (isBatch(state.flags)) {
910
- const entries = [];
911
- for (const entry of state.entries) {
912
- const validation = await validatePublishTarget(entry.target, { force: state.flags.force, lockfileOrg });
913
- if (cancelled)
914
- return;
915
- entries.push({ ...entry, validation });
916
- }
917
- const failed = [{ label: state.flags.rawPath, validation: result }, ...entries].filter((f) => f.validation && !f.validation.valid);
918
- if (failed.length > 0) {
919
- const lines = failed.flatMap((f) => [
920
- ` ${f.label}:`,
921
- ...(f.validation?.errors ?? []).map((e) => ` - ${e}`),
922
- ]);
923
- dispatch({ message: `Validation failed:\n${lines.join('\n')}`, type: 'ERROR' });
924
- return;
925
- }
926
- // checking-entries keeps these instead of validating each entry again.
927
- dispatch({ entries, type: 'ENTRIES_LOADED' });
928
- }
929
- dispatch({ lockfileOrgReviewers, type: 'VALIDATE_DONE', validation: result });
930
- };
931
- validate().catch((err) => {
932
- if (!cancelled) {
933
- dispatch(toErrorAction(err));
934
- }
44
+ const ask = (s) => new Promise((resolve) => {
45
+ typed.current = promptValue(s);
46
+ answer.current = resolve;
935
47
  });
48
+ const prompter = {
49
+ bump: ask,
50
+ confirmPublish: ask,
51
+ confirmUpdate: ask,
52
+ confirmWith: ask,
53
+ description: ask,
54
+ rawTags: ask,
55
+ reviewers: ask,
56
+ tags: ask,
57
+ tools: () => new Promise((resolve) => {
58
+ answerTools.current = resolve;
59
+ }),
60
+ };
61
+ // Unmounting aborts the run: it drops the pending prompt and removes the staging directories
62
+ const controller = new AbortController();
63
+ void runPublish(flags, prompter, { onState: setState, signal: controller.signal });
936
64
  return () => {
937
- cancelled = true;
65
+ controller.abort();
938
66
  };
939
- // Phase-gated effect: only runs when entering 'validating'
940
67
  // eslint-disable-next-line react-hooks/exhaustive-deps
941
- }, [state.phase]);
942
- // --- Phase: loading-tools ---
943
- // In interactive mode, loads adapters for the multi-select prompt.
944
- // In non-interactive mode, resolves tools via manifest -> --tools flag ->
945
- // detectCompatibleTools(assetType) -> exit 1, then dispatches
946
- // RESOLVE_TOOLS_NONINTERACTIVE to advance directly to checking-registry.
947
- useEffect(() => {
948
- if (state.phase !== 'loading-tools')
949
- return;
950
- let cancelled = false;
951
- const load = async () => {
952
- if (state.nonInteractive) {
953
- // Manifest tools win when present (VALIDATE_DONE only routes here when
954
- // manifest tools are missing for assets — bundles short-circuit hasTools).
955
- if (state.target?.type !== 'asset') {
956
- // Bundles don't carry tools — just advance.
957
- dispatch({ tools: [], type: 'RESOLVE_TOOLS_NONINTERACTIVE' });
958
- return;
959
- }
960
- // Try --tools flag
961
- let resolvedToolList = [];
962
- if (state.flags.toolsFlag) {
963
- resolvedToolList = state.flags.toolsFlag
964
- .split(',')
965
- .map((t) => t.trim())
966
- .filter(Boolean);
967
- // Validation ran before these tools were filled in, so check them here.
968
- const toolsError = await unknownToolsError(resolvedToolList);
969
- if (cancelled)
970
- return;
971
- if (toolsError) {
972
- dispatch({ message: `--tools: ${toolsError}`, type: 'ERROR' });
973
- return;
974
- }
975
- }
976
- // Fall back to detectCompatibleTools
977
- if (resolvedToolList.length === 0) {
978
- const assetType = state.target.assetType;
979
- if (!assetType) {
980
- dispatch({
981
- message: '--tools <comma-separated> required: no tools detected for type unknown',
982
- type: 'ERROR',
983
- });
984
- return;
985
- }
986
- const detected = await detectCompatibleTools(assetType);
987
- if (cancelled)
988
- return;
989
- if (detected.length === 0) {
990
- dispatch({
991
- message: `--tools <comma-separated> required: no tools detected for type ${assetType}`,
992
- type: 'ERROR',
993
- });
994
- return;
995
- }
996
- resolvedToolList = detected;
997
- }
998
- dispatch({ tools: resolvedToolList, type: 'RESOLVE_TOOLS_NONINTERACTIVE' });
999
- return;
1000
- }
1001
- const adapters = await loadAllAdapters();
1002
- if (cancelled)
1003
- return;
1004
- dispatch({ adapters, type: 'TOOLS_LOADED' });
1005
- };
1006
- load().catch((err) => {
1007
- if (!cancelled) {
1008
- dispatch(toErrorAction(err));
1009
- }
1010
- });
1011
- return () => {
1012
- cancelled = true;
1013
- };
1014
- // Phase-gated effect: only runs when entering 'loading-tools'
1015
- // eslint-disable-next-line react-hooks/exhaustive-deps
1016
- }, [state.phase]);
1017
- // --- Phase: checking-registry ---
1018
- useEffect(() => {
1019
- if (state.phase !== 'checking-registry')
1020
- return;
1021
- if (!state.target)
1022
- return;
1023
- let cancelled = false;
1024
- const check = async () => {
1025
- const authToken = (await getCredential()).token;
1026
- if (cancelled)
1027
- return;
1028
- const reg = await fetchRegistry({ force: state.flags.refresh });
1029
- if (cancelled)
1030
- return;
1031
- const versionResult = checkRegistryVersion(state.target, reg);
1032
- if (cancelled)
1033
- return;
1034
- let isUpdate;
1035
- let resolvedVersion = '';
1036
- if (versionResult.status === 'new-asset') {
1037
- isUpdate = false;
1038
- resolvedVersion = state.target.data.version;
1039
- }
1040
- else if (versionResult.status === 'version-already-bumped') {
1041
- isUpdate = true;
1042
- resolvedVersion = state.target.data.version;
1043
- }
1044
- else {
1045
- // needs-bump
1046
- isUpdate = true;
1047
- }
1048
- if (cancelled)
1049
- return;
1050
- dispatch({
1051
- isUpdate,
1052
- resolvedVersion,
1053
- token: authToken,
1054
- type: 'REGISTRY_CHECK_DONE',
1055
- versionCheck: versionResult,
1056
- });
1057
- };
1058
- check().catch((err) => {
1059
- if (!cancelled) {
1060
- dispatch(toErrorAction(err, mapPublishError(err)));
1061
- }
1062
- });
1063
- return () => {
1064
- cancelled = true;
1065
- };
1066
- // Phase-gated effect: only runs when entering 'checking-registry'
1067
- // eslint-disable-next-line react-hooks/exhaustive-deps
1068
- }, [state.phase]);
1069
- // --- Phase: checking-entries ---
1070
- // Validate each entry not yet checked the same way as the root asset, and check its
1071
- // version against the registry; advanceToPlan then settles any bump it needs.
1072
- useEffect(() => {
1073
- if (state.phase !== 'checking-entries')
1074
- return;
1075
- let cancelled = false;
1076
- const check = async () => {
1077
- let lockfileOrg;
1078
- try {
1079
- lockfileOrg = (await readLockfile(findProjectRoot(state.flags.projectRoot))).org;
1080
- }
1081
- catch {
1082
- // No project root / lockfile — treat as no lockfile org
1083
- }
1084
- const registry = await fetchRegistry({ force: state.flags.refresh });
1085
- if (cancelled)
1086
- return;
1087
- const entries = [];
1088
- for (const entry of state.entries) {
1089
- if (entry.versionCheck) {
1090
- entries.push(entry);
1091
- continue;
1092
- }
1093
- const validation = entry.validation ?? (await validatePublishTarget(entry.target, { force: state.flags.force, lockfileOrg }));
1094
- if (cancelled)
1095
- return;
1096
- if (!validation.valid) {
1097
- dispatch({
1098
- message: `Validation failed for ${entry.label}:\n${validation.errors.map((e) => ` - ${e}`).join('\n')}`,
1099
- type: 'ERROR',
1100
- });
1101
- return;
1102
- }
1103
- const versionCheck = checkRegistryVersion(entry.target, registry);
1104
- entries.push({
1105
- ...entry,
1106
- ...(versionCheck.status === 'needs-bump' ? {} : { resolvedVersion: entry.target.data.version }),
1107
- validation,
1108
- versionCheck,
1109
- });
1110
- }
1111
- dispatch({ entries, type: 'ENTRIES_CHECKED' });
1112
- };
1113
- check().catch((err) => {
1114
- if (!cancelled) {
1115
- dispatch(toErrorAction(err, mapPublishError(err)));
1116
- }
1117
- });
1118
- return () => {
1119
- cancelled = true;
1120
- };
1121
- // Phase-gated effect: only runs when entering 'checking-entries'
1122
- // eslint-disable-next-line react-hooks/exhaustive-deps
1123
- }, [state.phase]);
1124
- // --- Phase: building-plan ---
1125
- // Plans from the targets in memory, which carry every bump and prompted tag or tool:
1126
- // nothing is written to a manifest until the publish succeeds (writeManifestEdits).
1127
- useEffect(() => {
1128
- if (state.phase !== 'building-plan')
1129
- return;
1130
- if (!state.target)
1131
- return;
1132
- let cancelled = false;
1133
- const build = async () => {
1134
- const currentTarget = state.target;
1135
- const currentVersion = state.resolvedVersion;
1136
- // Reject invalid reviewer usernames here, before any branch/commit/push work happens
1137
- const reviewers = parseReviewersInput(state.reviewerValue);
1138
- for (const reviewer of reviewers) {
1139
- const check = validateGithubUsername(reviewer);
1140
- if (!check.valid) {
1141
- throw new Error(check.error);
1142
- }
1143
- }
1144
- const entries = state.entries.map((entry) => ({ label: entry.label, target: entry.target }));
1145
- const localPlan = await buildPublishPlan(currentTarget, currentVersion, state.isUpdate, state.flags.message, reviewers, entries, ...(isBatch(state.flags) ? [state.flags.rawPath] : []));
1146
- if (cancelled)
1147
- return;
1148
- // Resolve declared dependencies against the registry now, so the preview and
1149
- // --dry-run show them and an unresolvable one stops before the confirm prompt.
1150
- const publishPlan = await resolvePlanDependencies(localPlan);
1151
- if (cancelled)
1152
- return;
1153
- let manifestDiff;
1154
- if (state.isUpdate && state.versionCheck && state.versionCheck.status !== 'new-asset' && state.token) {
1155
- const existing = await fetchExistingManifest(currentTarget, state.versionCheck.latestVersion);
1156
- if (cancelled)
1157
- return;
1158
- if (existing) {
1159
- manifestDiff = diffManifests(existing, currentTarget.data);
1160
- }
1161
- }
1162
- dispatch({ manifestDiff, plan: publishPlan, type: 'PLAN_DONE' });
1163
- };
1164
- build().catch(async (err) => {
1165
- if (cancelled)
1166
- return;
1167
- // Worded like the publishing phase: resolvePlanDependencies calls the same API.
1168
- const error = toErrorAction(err, mapPublishError(err, entryLabels(state), batchRootLabel(state)));
1169
- const suggestion = await suggestSiblingEntries(state, err).catch(() => undefined);
1170
- if (cancelled)
1171
- return;
1172
- dispatch(suggestion ? { error, ...suggestion, type: 'SUGGEST_ENTRIES' } : error);
1173
- });
1174
- return () => {
1175
- cancelled = true;
1176
- };
1177
- // Phase-gated effect: only runs when entering 'building-plan'
1178
- // eslint-disable-next-line react-hooks/exhaustive-deps
1179
- }, [state.phase]);
1180
- // --- Phase: publishing ---
1181
- useEffect(() => {
1182
- if (state.phase !== 'publishing')
1183
- return;
1184
- if (!state.plan || !state.token)
1185
- return;
1186
- let cancelled = false;
1187
- const publish = async () => {
1188
- const progressCallback = (msg) => {
1189
- if (!cancelled) {
1190
- dispatch({ message: msg, type: 'PUBLISH_PROGRESS' });
1191
- }
1192
- };
1193
- const published = await executePublish(state.plan, progressCallback);
1194
- // The PR is open, so the folders take its versions, tags and tools even if the run was cancelled meanwhile.
1195
- const unwritten = await writeManifestEdits(state);
1196
- if (cancelled)
1197
- return;
1198
- const result = unwritten.length > 0 ? { ...published, warnings: [...(published.warnings ?? []), ...unwritten] } : published;
1199
- // Clear registry cache so subsequent commands see the new version
1200
- try {
1201
- await clearCache();
1202
- }
1203
- catch {
1204
- // Best-effort — don't fail publish over cache
1205
- }
1206
- // Update lockfile in-place when publishing from an installed asset
1207
- if (state.flags.fromInstalled || state.flags.update) {
1208
- try {
1209
- const projectRoot = findProjectRoot(state.flags.projectRoot);
1210
- const pubTools = await resolveTools(state.flags.tool);
1211
- const pubAdapter = pubTools[0].adapter;
1212
- await withLockfileLock(projectRoot, async () => {
1213
- const lockfile = await readLockfile(projectRoot);
1214
- const { name, org } = parseOrgFromName(state.flags.rawPath);
1215
- const installed = findInstalledAsset(lockfile, name, undefined, org);
1216
- if (installed) {
1217
- const resolved = resolveInstalledPaths(installed, pubAdapter, projectRoot);
1218
- const updatedFiles = await Promise.all(resolved.files.map(async (file) => ({
1219
- checksum: await hashFile(join(projectRoot, file.installedPath)),
1220
- sourcePath: file.sourcePath,
1221
- })));
1222
- const updatedLockfile = addAssetToLockfile(lockfile, {
1223
- ...installed,
1224
- files: updatedFiles,
1225
- version: state.resolvedVersion,
1226
- });
1227
- await writeLockfile(projectRoot, updatedLockfile);
1228
- }
1229
- });
1230
- }
1231
- catch {
1232
- // Best-effort — don't fail publish over lockfile update
1233
- }
1234
- }
1235
- if (cancelled)
1236
- return;
1237
- dispatch({ result, type: 'PUBLISH_DONE' });
1238
- };
1239
- publish().catch((err) => {
1240
- if (!cancelled) {
1241
- dispatch(toErrorAction(err, mapPublishError(err, entryLabels(state), batchRootLabel(state))));
1242
- }
1243
- });
1244
- return () => {
1245
- cancelled = true;
1246
- };
1247
- // Phase-gated effect: only runs when entering 'publishing'
1248
- // eslint-disable-next-line react-hooks/exhaustive-deps
1249
- }, [state.phase]);
1250
- // --- Phase: installing (auto-install raw publish to lockfile) ---
1251
- useEffect(() => {
1252
- if (state.phase !== 'installing')
1253
- return;
1254
- let cancelled = false;
1255
- const install = async () => {
1256
- try {
1257
- const projectRoot = findProjectRoot(state.flags.projectRoot);
1258
- await withLockfileLock(projectRoot, async () => {
1259
- // Read current lockfile
1260
- const lockfile = await readLockfile(projectRoot);
1261
- // Compute checksums from actual project files (not staging copies)
1262
- const files = [];
1263
- for (const file of state.rawDetection.files) {
1264
- const checksum = await hashFile(file.absolutePath);
1265
- files.push({
1266
- checksum,
1267
- sourcePath: file.relativePath,
1268
- });
1269
- }
1270
- // Build InstalledAsset entry
1271
- const installedAsset = {
1272
- files,
1273
- installReason: 'direct',
1274
- name: state.rawDetection.name,
1275
- org: state.orgValue || undefined,
1276
- type: state.rawDetection.assetType,
1277
- version: state.resolvedVersion || state.target?.data.version || '1.0.0',
1278
- };
1279
- // Add to lockfile and write
1280
- const updatedLockfile = addAssetToLockfile(lockfile, installedAsset);
1281
- await writeLockfile(projectRoot, updatedLockfile);
1282
- });
1283
- if (!cancelled) {
1284
- dispatch({ type: 'INSTALL_DONE' });
1285
- }
1286
- }
1287
- catch {
1288
- // Publish already succeeded, so just warn and move to done
1289
- if (!cancelled) {
1290
- dispatch({ type: 'INSTALL_DONE' });
1291
- }
1292
- }
1293
- };
1294
- install();
1295
- return () => {
1296
- cancelled = true;
1297
- };
1298
- // Phase-gated effect: only runs when entering 'installing'
1299
- // eslint-disable-next-line react-hooks/exhaustive-deps
1300
- }, [state.phase]);
1301
- // --- Cleanup temp directory on error or done ---
1302
- useEffect(() => {
1303
- if (state.phase !== 'error' && state.phase !== 'done' && state.phase !== 'dry-run-result')
1304
- return;
1305
- if (tempDirRef.current) {
1306
- const dir = tempDirRef.current;
1307
- tempDirRef.current = null;
1308
- rm(dir, { force: true, recursive: true }).catch(() => {
1309
- // Best-effort cleanup — ignore errors
1310
- });
1311
- }
1312
- }, [state.phase]);
1313
- return [state, dispatch];
1314
- }
1315
- // ---------------------------------------------------------------------------
1316
- // Pure reducer
1317
- // ---------------------------------------------------------------------------
1318
- /**
1319
- * The step before `building-plan`, once the root asset's own checks are done:
1320
- * check the entries not yet checked, then settle each entry that needs a bump
1321
- * (the bump prompt; `--bump` or an error when non-interactive), then plan.
1322
- */
1323
- function advanceToPlan(state) {
1324
- if (state.entries.some((entry) => !entry.versionCheck)) {
1325
- return { ...state, bumpEntry: undefined, phase: 'checking-entries' };
1326
- }
1327
- // A batch without --bump names every asset that needs one, not just the first.
1328
- if (isBatch(state.flags) && state.nonInteractive && !state.flags.bump) {
1329
- const pending = [
1330
- ...(state.target && state.versionCheck?.status === 'needs-bump' && !state.resolvedVersion
1331
- ? [{ label: state.flags.rawPath, latestVersion: state.versionCheck.latestVersion, target: state.target }]
1332
- : []),
1333
- ...state.entries.flatMap((entry) => entry.versionCheck?.status === 'needs-bump' && entry.resolvedVersion === undefined
1334
- ? [{ label: entry.label, latestVersion: entry.versionCheck.latestVersion, target: entry.target }]
1335
- : []),
1336
- ];
1337
- if (pending.length > 0) {
1338
- const lines = pending.map((p) => ` ${p.label}: ${p.target.assetType}:${p.target.data.name} v${p.target.data.version}, registry has v${p.latestVersion}`);
1339
- return {
1340
- ...state,
1341
- errorMessage: `--bump <patch|minor|major> required: these manifests are not newer than the registry:\n${lines.join('\n')}`,
1342
- phase: 'error',
1343
- };
1344
- }
1345
- }
1346
- const index = state.entries.findIndex((entry) => entry.versionCheck?.status === 'needs-bump' && entry.resolvedVersion === undefined);
1347
- const entry = state.entries[index];
1348
- if (entry?.versionCheck?.status !== 'needs-bump') {
1349
- return { ...state, bumpEntry: undefined, phase: 'building-plan' };
1350
- }
1351
- if (!state.nonInteractive) {
1352
- return { ...state, bumpEntry: index, bumpSelection: '', phase: 'bump-prompt' };
1353
- }
1354
- const { latestVersion } = entry.versionCheck;
1355
- if (!state.flags.bump) {
1356
- return {
1357
- ...state,
1358
- errorMessage: `--bump <patch|minor|major> required: registry has v${latestVersion} of ${entry.target.assetType}:${entry.target.data.name} (from ${entry.label}) but its manifest is not newer`,
1359
- phase: 'error',
1360
- };
1361
- }
1362
- return bumpEntryAt(state, index, state.flags.bump);
1363
- }
1364
- /** Why the flags can't publish a batch: one installed asset, or one asset's metadata. Undefined otherwise. */
1365
- function batchFlagError(flags) {
1366
- if (!isBatch(flags))
1367
- return undefined;
1368
- if (flags.update || flags.fromInstalled) {
1369
- return `${flags.update ? '--update' : '--from-installed'} publishes one installed asset; leave it out to publish several folders.`;
1370
- }
1371
- const perAsset = [
1372
- ['--description', flags.descriptionFlag],
1373
- ['--tags', flags.tagsFlag],
1374
- ['--tools', flags.toolsFlag],
1375
- ].flatMap(([flag, value]) => (value === undefined ? [] : [flag]));
1376
- if (perAsset.length > 0) {
1377
- return `${perAsset.join(', ')} ${perAsset.length === 1 ? 'sets' : 'set'} one asset's metadata; put it in each folder's manifest.json to publish several folders.`;
1378
- }
1379
- return undefined;
1380
- }
1381
- /** In a batch, what the API calls `Root asset`, as the folder the user named. */
1382
- function batchRootLabel(state) {
1383
- return isBatch(state.flags) ? state.flags.rawPath : undefined;
1384
- }
1385
- /**
1386
- * Bump the entry at `index` from its registry version, then go on toward the
1387
- * plan; an error state, as for the root, when the version can't be bumped.
1388
- */
1389
- function bumpEntryAt(state, index, bump) {
1390
- const entry = state.entries[index];
1391
- if (entry?.versionCheck?.status !== 'needs-bump')
1392
- return state;
1393
- let version;
1394
- try {
1395
- version = bumpVersion(entry.versionCheck.latestVersion, bump);
1396
- }
1397
- catch {
1398
- return { ...state, errorMessage: `Failed to compute bumped version for ${entry.label}.`, phase: 'error' };
1399
- }
1400
- return advanceToPlan({ ...state, entries: replaceEntry(state.entries, index, withVersion(entry, version)) });
1401
- }
1402
- /** What the API calls `dependencies[i]`, as the folder the user named. */
1403
- function entryLabels(state) {
1404
- return state.entries.map((entry) => entry.label);
1405
- }
1406
- // ---------------------------------------------------------------------------
1407
- // Hook
1408
- // ---------------------------------------------------------------------------
1409
- /** Run the writes in order until one throws: its index and error, or undefined when all succeed. */
1410
- async function firstFailure(edits) {
1411
- for (const [index, edit] of edits.entries()) {
1412
- try {
1413
- await edit.write();
1414
- }
1415
- catch (error) {
1416
- return { error, index };
1417
- }
1418
- }
1419
- return undefined;
1420
- }
1421
- /**
1422
- * Phase to enter once the tags/tools metadata prompts are resolved: org-scoped
1423
- * targets get the reviewer prompt, everything else proceeds to the registry check.
1424
- */
1425
- function phaseAfterMetadata(target) {
1426
- return target && isOrgScopedTarget(target) ? 'prompting-reviewer' : 'checking-registry';
1427
- }
1428
- function replaceEntry(entries, index, entry) {
1429
- return entries.map((current, i) => (i === index ? entry : current));
1430
- }
1431
- /**
1432
- * Folders next to the root asset that could fix the plan's error: an interactive
1433
- * run, a `422` naming root dependencies that aren't in the registry (or have no
1434
- * matching version), and a sibling folder whose manifest has the same type and
1435
- * name that isn't an entry yet. Undefined when there is nothing to offer.
1436
- * `--update` and `--from-installed` roots live in a temp folder, so they never get offers.
1437
- */
1438
- async function suggestSiblingEntries(state, err) {
1439
- const { target } = state;
1440
- if (state.nonInteractive ||
1441
- state.flags.fromInstalled ||
1442
- state.flags.update ||
1443
- target?.type !== 'asset' ||
1444
- !(isApiError(err, 'dependency_not_found') || isApiError(err, 'dependency_version_unsatisfied'))) {
1445
- return undefined;
1446
- }
1447
- const included = (d) => state.entries.some((e) => e.target.assetType === d.type && e.target.data.name === d.name);
1448
- const missing = missingDependencies(target.data, err.issues).filter((d) => !included(d));
1449
- if (missing.length === 0)
1450
- return undefined;
1451
- const rootDir = state.rawDetection?.sourceDir ?? target.sourceDir;
1452
- const suggested = await findSiblingDependencies(rootDir, missing, state.entries.map((e) => e.target.sourceDir), target.data.org);
1453
- return suggested.length > 0 ? { missing, suggested } : undefined;
1454
- }
1455
- /**
1456
- * Build an ERROR dispatch action from a caught value, carrying the `code`,
1457
- * `hint` and `command` typed errors have so commands can render actionable
1458
- * recovery guidance.
1459
- */
1460
- function toErrorAction(err, override) {
1461
- return {
1462
- ...errorGuidance(err),
1463
- message: override ?? errorMessage(err),
1464
- type: 'ERROR',
68
+ }, []);
69
+ const input = {
70
+ onChange: (text) => {
71
+ typed.current = text;
72
+ setEdit({ for: state, text });
73
+ },
74
+ onSubmit: () => {
75
+ const resolve = answer.current;
76
+ answer.current = undefined;
77
+ resolve?.(typed.current);
78
+ },
79
+ value: edit?.for === state ? edit.text : promptValue(state),
1465
80
  };
1466
- }
1467
- /** The entry published at `version`; a successful publish writes it to the entry's manifest. */
1468
- function withVersion(entry, version) {
1469
- return {
1470
- ...entry,
1471
- resolvedVersion: version,
1472
- target: { ...entry.target, data: { ...entry.target.data, version } },
81
+ const pick = (action) => {
82
+ ticked.current = toolPickerReducer(ticked.current, action);
83
+ setPicker(ticked.current);
1473
84
  };
1474
- }
1475
- /**
1476
- * Write what the run changed in memory to the manifests on disk: the root's bump and each
1477
- * entry's, and the tags and tools the prompts asked for. Only once the publish has opened its
1478
- * PR, so a cancelled or failed one leaves every manifest as it was. A file whose write fails
1479
- * takes none of its later edits and comes back as one warning naming the fields it lacks; the
1480
- * other files are still written.
1481
- */
1482
- async function writeManifestEdits(state) {
1483
- const target = state.target;
1484
- if (!target)
1485
- return [];
1486
- const files = new Map();
1487
- const edit = (dir, file, field, value, write) => {
1488
- const path = join(dir, file);
1489
- files.set(path, [...(files.get(path) ?? []), { field, value, write }]);
85
+ const count = state.toolAdapters.length;
86
+ const toolPicker = {
87
+ ...picker,
88
+ confirm: () => {
89
+ if (ticked.current.selected.length === 0)
90
+ return;
91
+ const resolve = answerTools.current;
92
+ answerTools.current = undefined;
93
+ resolve?.(ticked.current.selected);
94
+ },
95
+ down: () => pick({ count, type: 'TOOL_CURSOR_DOWN' }),
96
+ toggle: () => {
97
+ const tool = state.toolAdapters[ticked.current.cursor]?.tool;
98
+ if (tool)
99
+ pick({ tool, type: 'TOGGLE_PROMPT_TOOL' });
100
+ },
101
+ up: () => pick({ count, type: 'TOOL_CURSOR_UP' }),
1490
102
  };
1491
- const version = (dir, file, value) => edit(dir, file, 'version', value, () => updateManifestVersion(dir, file, value));
1492
- const field = (name, value) => edit(target.sourceDir, 'manifest.json', name, value, () => updateManifestField(target.sourceDir, 'manifest.json', name, value));
1493
- if (state.versionCheck?.status === 'needs-bump' && state.resolvedVersion) {
1494
- version(target.sourceDir, target.type === 'asset' ? 'manifest.json' : 'bundle.json', state.resolvedVersion);
1495
- }
1496
- // Only an asset's manifest.json takes the prompted tags and tools, as the target (and so the plan) has them.
1497
- if (target.type === 'asset') {
1498
- const { tags, tools } = target.data;
1499
- if (state.promptTagsValue && tags && tags.length > 0) {
1500
- field('tags', tags);
1501
- }
1502
- if (state.selectedPromptTools.length > 0) {
1503
- field('tools', tools);
1504
- }
1505
- }
1506
- for (const entry of state.entries) {
1507
- if (entry.versionCheck?.status === 'needs-bump' && entry.resolvedVersion) {
1508
- version(entry.target.sourceDir, 'manifest.json', entry.resolvedVersion);
1509
- }
1510
- }
1511
- const unwritten = [];
1512
- for (const [path, edits] of files) {
1513
- const failed = await firstFailure(edits);
1514
- if (!failed)
1515
- continue;
1516
- const missing = edits.slice(failed.index);
1517
- // Only the version matters to the next publish: it asks for missing tags and tools again.
1518
- const again = missing.some((e) => e.field === 'version') ? ' before you publish it again' : '';
1519
- 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}.`);
1520
- }
1521
- return unwritten;
103
+ return { input, picker: toolPicker, state };
1522
104
  }
1523
105
  //# sourceMappingURL=usePublishState.js.map