@detergent-software/atk 3.0.0-dev.1 → 3.0.0-dev.10

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 (181) hide show
  1. package/README.md +361 -0
  2. package/build/commands/audit.d.ts.map +1 -1
  3. package/build/commands/audit.js +77 -37
  4. package/build/commands/audit.js.map +1 -1
  5. package/build/commands/browse.d.ts.map +1 -1
  6. package/build/commands/browse.js +50 -115
  7. package/build/commands/browse.js.map +1 -1
  8. package/build/commands/diff.d.ts.map +1 -1
  9. package/build/commands/diff.js +2 -1
  10. package/build/commands/diff.js.map +1 -1
  11. package/build/commands/info.d.ts.map +1 -1
  12. package/build/commands/info.js +1 -1
  13. package/build/commands/info.js.map +1 -1
  14. package/build/commands/init.d.ts.map +1 -1
  15. package/build/commands/init.js +39 -405
  16. package/build/commands/init.js.map +1 -1
  17. package/build/commands/install.d.ts.map +1 -1
  18. package/build/commands/install.js +46 -31
  19. package/build/commands/install.js.map +1 -1
  20. package/build/commands/list.d.ts +2 -2
  21. package/build/commands/list.d.ts.map +1 -1
  22. package/build/commands/list.js +131 -27
  23. package/build/commands/list.js.map +1 -1
  24. package/build/commands/outdated.d.ts.map +1 -1
  25. package/build/commands/outdated.js +7 -6
  26. package/build/commands/outdated.js.map +1 -1
  27. package/build/commands/prune.d.ts.map +1 -1
  28. package/build/commands/prune.js +35 -47
  29. package/build/commands/prune.js.map +1 -1
  30. package/build/commands/publish.d.ts +3 -1
  31. package/build/commands/publish.d.ts.map +1 -1
  32. package/build/commands/publish.js +67 -390
  33. package/build/commands/publish.js.map +1 -1
  34. package/build/commands/setup.d.ts +1 -1
  35. package/build/commands/setup.d.ts.map +1 -1
  36. package/build/commands/setup.js +49 -242
  37. package/build/commands/setup.js.map +1 -1
  38. package/build/commands/sync.d.ts.map +1 -1
  39. package/build/commands/sync.js +111 -120
  40. package/build/commands/sync.js.map +1 -1
  41. package/build/commands/uninstall.d.ts.map +1 -1
  42. package/build/commands/uninstall.js +103 -218
  43. package/build/commands/uninstall.js.map +1 -1
  44. package/build/commands/update.d.ts.map +1 -1
  45. package/build/commands/update.js +30 -23
  46. package/build/commands/update.js.map +1 -1
  47. package/build/components/AssetTable.d.ts +3 -1
  48. package/build/components/AssetTable.d.ts.map +1 -1
  49. package/build/components/AssetTable.js +30 -37
  50. package/build/components/AssetTable.js.map +1 -1
  51. package/build/components/DryRunBanner.d.ts +3 -3
  52. package/build/components/DryRunBanner.d.ts.map +1 -1
  53. package/build/components/DryRunBanner.js +2 -2
  54. package/build/components/DryRunBanner.js.map +1 -1
  55. package/build/components/FilterBar.d.ts +10 -3
  56. package/build/components/FilterBar.d.ts.map +1 -1
  57. package/build/components/FilterBar.js +4 -4
  58. package/build/components/FilterBar.js.map +1 -1
  59. package/build/components/InitSuccess.d.ts +5 -0
  60. package/build/components/InitSuccess.d.ts.map +1 -0
  61. package/build/components/InitSuccess.js +20 -0
  62. package/build/components/InitSuccess.js.map +1 -0
  63. package/build/components/InstallSummary.d.ts +3 -3
  64. package/build/components/InstallSummary.d.ts.map +1 -1
  65. package/build/components/InstallSummary.js +2 -2
  66. package/build/components/InstallSummary.js.map +1 -1
  67. package/build/components/Link.d.ts +7 -0
  68. package/build/components/Link.d.ts.map +1 -0
  69. package/build/components/Link.js +6 -0
  70. package/build/components/Link.js.map +1 -0
  71. package/build/components/ListActionBar.d.ts +9 -0
  72. package/build/components/ListActionBar.d.ts.map +1 -0
  73. package/build/components/ListActionBar.js +30 -0
  74. package/build/components/ListActionBar.js.map +1 -0
  75. package/build/components/ListBrowseList.d.ts +25 -0
  76. package/build/components/ListBrowseList.d.ts.map +1 -0
  77. package/build/components/ListBrowseList.js +55 -0
  78. package/build/components/ListBrowseList.js.map +1 -0
  79. package/build/components/ListExpandedRow.d.ts +36 -0
  80. package/build/components/ListExpandedRow.d.ts.map +1 -0
  81. package/build/components/ListExpandedRow.js +46 -0
  82. package/build/components/ListExpandedRow.js.map +1 -0
  83. package/build/components/ListHelpBar.d.ts +19 -0
  84. package/build/components/ListHelpBar.d.ts.map +1 -0
  85. package/build/components/ListHelpBar.js +11 -0
  86. package/build/components/ListHelpBar.js.map +1 -0
  87. package/build/components/PromptField.d.ts +19 -0
  88. package/build/components/PromptField.d.ts.map +1 -0
  89. package/build/components/PromptField.js +7 -0
  90. package/build/components/PromptField.js.map +1 -0
  91. package/build/components/StatusBadge.d.ts +1 -1
  92. package/build/components/StatusBadge.d.ts.map +1 -1
  93. package/build/components/StatusBadge.js +4 -0
  94. package/build/components/StatusBadge.js.map +1 -1
  95. package/build/components/index.d.ts +7 -0
  96. package/build/components/index.d.ts.map +1 -1
  97. package/build/components/index.js +7 -0
  98. package/build/components/index.js.map +1 -1
  99. package/build/hooks/useBrowseState.d.ts +23 -0
  100. package/build/hooks/useBrowseState.d.ts.map +1 -1
  101. package/build/hooks/useBrowseState.js +77 -1
  102. package/build/hooks/useBrowseState.js.map +1 -1
  103. package/build/hooks/useConfirmation.d.ts +18 -0
  104. package/build/hooks/useConfirmation.d.ts.map +1 -0
  105. package/build/hooks/useConfirmation.js +56 -0
  106. package/build/hooks/useConfirmation.js.map +1 -0
  107. package/build/hooks/useInitState.d.ts +72 -0
  108. package/build/hooks/useInitState.d.ts.map +1 -0
  109. package/build/hooks/useInitState.js +372 -0
  110. package/build/hooks/useInitState.js.map +1 -0
  111. package/build/hooks/useListActions.d.ts +14 -0
  112. package/build/hooks/useListActions.d.ts.map +1 -0
  113. package/build/hooks/useListActions.js +151 -0
  114. package/build/hooks/useListActions.js.map +1 -0
  115. package/build/hooks/useListState.d.ts +147 -0
  116. package/build/hooks/useListState.d.ts.map +1 -0
  117. package/build/hooks/useListState.js +445 -0
  118. package/build/hooks/useListState.js.map +1 -0
  119. package/build/hooks/usePublishState.d.ts +115 -0
  120. package/build/hooks/usePublishState.d.ts.map +1 -0
  121. package/build/hooks/usePublishState.js +673 -0
  122. package/build/hooks/usePublishState.js.map +1 -0
  123. package/build/hooks/useSetupState.d.ts +59 -0
  124. package/build/hooks/useSetupState.d.ts.map +1 -0
  125. package/build/hooks/useSetupState.js +295 -0
  126. package/build/hooks/useSetupState.js.map +1 -0
  127. package/build/hooks/useUninstallState.d.ts +102 -0
  128. package/build/hooks/useUninstallState.d.ts.map +1 -0
  129. package/build/hooks/useUninstallState.js +342 -0
  130. package/build/hooks/useUninstallState.js.map +1 -0
  131. package/build/lib/checksum.d.ts +39 -0
  132. package/build/lib/checksum.d.ts.map +1 -1
  133. package/build/lib/checksum.js +101 -13
  134. package/build/lib/checksum.js.map +1 -1
  135. package/build/lib/config.d.ts +4 -1
  136. package/build/lib/config.d.ts.map +1 -1
  137. package/build/lib/config.js +9 -9
  138. package/build/lib/config.js.map +1 -1
  139. package/build/lib/diagnostics.js +1 -1
  140. package/build/lib/diagnostics.js.map +1 -1
  141. package/build/lib/format.d.ts +6 -0
  142. package/build/lib/format.d.ts.map +1 -0
  143. package/build/lib/format.js +18 -0
  144. package/build/lib/format.js.map +1 -0
  145. package/build/lib/github.d.ts +8 -0
  146. package/build/lib/github.d.ts.map +1 -1
  147. package/build/lib/github.js +9 -0
  148. package/build/lib/github.js.map +1 -1
  149. package/build/lib/installer.d.ts +1 -0
  150. package/build/lib/installer.d.ts.map +1 -1
  151. package/build/lib/installer.js +7 -7
  152. package/build/lib/installer.js.map +1 -1
  153. package/build/lib/lockfile.d.ts.map +1 -1
  154. package/build/lib/lockfile.js +1 -4
  155. package/build/lib/lockfile.js.map +1 -1
  156. package/build/lib/publisher.d.ts +35 -0
  157. package/build/lib/publisher.d.ts.map +1 -1
  158. package/build/lib/publisher.js +210 -16
  159. package/build/lib/publisher.js.map +1 -1
  160. package/build/lib/schemas/config.d.ts +7 -3
  161. package/build/lib/schemas/config.d.ts.map +1 -1
  162. package/build/lib/schemas/config.js +12 -2
  163. package/build/lib/schemas/config.js.map +1 -1
  164. package/build/lib/schemas/index.d.ts +1 -1
  165. package/build/lib/schemas/index.d.ts.map +1 -1
  166. package/build/lib/schemas/index.js +1 -1
  167. package/build/lib/schemas/index.js.map +1 -1
  168. package/build/lib/schemas/lockfile.d.ts +18 -0
  169. package/build/lib/schemas/lockfile.d.ts.map +1 -1
  170. package/build/lib/schemas/lockfile.js +10 -0
  171. package/build/lib/schemas/lockfile.js.map +1 -1
  172. package/build/lib/tool-resolver.d.ts +17 -6
  173. package/build/lib/tool-resolver.d.ts.map +1 -1
  174. package/build/lib/tool-resolver.js +31 -15
  175. package/build/lib/tool-resolver.js.map +1 -1
  176. package/build/lib/updater.d.ts.map +1 -1
  177. package/build/lib/updater.js +2 -1
  178. package/build/lib/updater.js.map +1 -1
  179. package/package.json +78 -65
  180. package/tool-adapters/claude-code.json +1 -1
  181. package/tool-adapters/copilot-cli.json +1 -1
@@ -0,0 +1,673 @@
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 { resolveInstalledPaths } from '../lib/adapter.js';
6
+ import { getGitHubToken } from '../lib/auth.js';
7
+ import { hashFile } from '../lib/checksum.js';
8
+ import { detectCompatibleTools, getGitUserName } from '../lib/init.js';
9
+ import { addAssetToLockfile, findInstalledAsset, readLockfile, withLockfileLock, writeLockfile, } from '../lib/lockfile.js';
10
+ import { parseOrgFromName } from '../lib/org.js';
11
+ import { findProjectRoot } from '../lib/paths.js';
12
+ import { buildPublishPlan, buildStagingArea, bumpVersion, checkRegistryVersion, detectAssetFromPath, detectPublishType, executeDirectPublish, executePublish, isOrgAsset, mapPublishError, updateManifestVersion, validatePublishTarget, } from '../lib/publisher.js';
13
+ import { clearCache, fetchRegistry } from '../lib/registry.js';
14
+ import { resolveTools } from '../lib/tool-resolver.js';
15
+ // ---------------------------------------------------------------------------
16
+ // Initial state factory
17
+ // ---------------------------------------------------------------------------
18
+ export function createInitialState(flags) {
19
+ return {
20
+ authorValue: '',
21
+ bumpSelection: '',
22
+ confirmValue: '',
23
+ descriptionValue: '',
24
+ errorMessage: '',
25
+ flags,
26
+ isRawPublish: false,
27
+ isUpdate: false,
28
+ metadataSubPhase: 'description',
29
+ orgValue: '',
30
+ phase: flags.fromInstalled ? 'resolving-installed' : 'detecting',
31
+ plan: undefined,
32
+ progressMessage: '',
33
+ publishResult: undefined,
34
+ rawDetection: null,
35
+ resolvedPath: flags.fromInstalled ? '' : resolve(flags.rawPath),
36
+ resolvedVersion: '',
37
+ tagsValue: '',
38
+ target: undefined,
39
+ token: undefined,
40
+ toolsDetected: [],
41
+ validation: undefined,
42
+ versionCheck: undefined,
43
+ };
44
+ }
45
+ // ---------------------------------------------------------------------------
46
+ // Pure reducer
47
+ // ---------------------------------------------------------------------------
48
+ export function publishReducer(state, action) {
49
+ switch (action.type) {
50
+ case 'BUMP_DONE':
51
+ return {
52
+ ...state,
53
+ errorMessage: action.message ? action.message : state.errorMessage,
54
+ phase: action.message ? 'error' : 'building-plan',
55
+ resolvedVersion: action.version,
56
+ target: action.updatedTarget,
57
+ };
58
+ case 'DETECT_DONE':
59
+ return {
60
+ ...state,
61
+ phase: 'validating',
62
+ target: action.target,
63
+ };
64
+ case 'ERROR':
65
+ return { ...state, errorMessage: action.message, phase: 'error' };
66
+ case 'INSTALL_DONE':
67
+ return { ...state, phase: 'done' };
68
+ case 'PLAN_DONE': {
69
+ const nextPhase = state.flags.dryRun ? 'dry-run-result' : 'publishing';
70
+ return {
71
+ ...state,
72
+ phase: nextPhase,
73
+ plan: action.plan,
74
+ };
75
+ }
76
+ case 'PUBLISH_DONE': {
77
+ const nextPhase = state.isRawPublish ? 'installing' : 'done';
78
+ return {
79
+ ...state,
80
+ phase: nextPhase,
81
+ publishResult: action.result,
82
+ };
83
+ }
84
+ case 'PUBLISH_PROGRESS':
85
+ return { ...state, progressMessage: action.message };
86
+ case 'RAW_DETECT_DONE':
87
+ return {
88
+ ...state,
89
+ authorValue: action.authorValue,
90
+ isRawPublish: true,
91
+ orgValue: action.orgValue,
92
+ phase: 'gathering-metadata',
93
+ rawDetection: action.rawDetection,
94
+ toolsDetected: action.toolsDetected,
95
+ };
96
+ case 'RAW_DETECT_DONE_NON_INTERACTIVE':
97
+ return {
98
+ ...state,
99
+ isRawPublish: true,
100
+ phase: 'validating',
101
+ rawDetection: action.rawDetection,
102
+ target: action.target,
103
+ };
104
+ case 'REGISTRY_CHECK_DONE': {
105
+ const { isUpdate, resolvedVersion, token, versionCheck } = action;
106
+ let nextPhase;
107
+ if (versionCheck.status === 'new-asset') {
108
+ nextPhase = 'building-plan';
109
+ }
110
+ else if (versionCheck.status === 'version-already-bumped') {
111
+ nextPhase = 'confirm-update';
112
+ }
113
+ else {
114
+ // needs-bump
115
+ nextPhase = 'bump-prompt';
116
+ }
117
+ return {
118
+ ...state,
119
+ isUpdate,
120
+ phase: nextPhase,
121
+ resolvedVersion,
122
+ token,
123
+ versionCheck,
124
+ };
125
+ }
126
+ case 'RESOLVED_INSTALLED':
127
+ return {
128
+ ...state,
129
+ phase: 'detecting',
130
+ resolvedPath: action.resolvedPath,
131
+ };
132
+ case 'SET_BUMP_SELECTION':
133
+ return { ...state, bumpSelection: action.value };
134
+ case 'SET_CONFIRM_VALUE':
135
+ return { ...state, confirmValue: action.value };
136
+ case 'SET_DESCRIPTION':
137
+ return { ...state, descriptionValue: action.value };
138
+ case 'SET_TAGS':
139
+ return { ...state, tagsValue: action.value };
140
+ case 'STAGING_DONE':
141
+ return {
142
+ ...state,
143
+ phase: 'validating',
144
+ target: action.target,
145
+ };
146
+ case 'SUBMIT_BUMP': {
147
+ // Validation happens in the effect; reducer just acknowledges intent
148
+ // The effect will dispatch BUMP_DONE or ERROR
149
+ const trimmed = state.bumpSelection.trim().toLowerCase();
150
+ if (trimmed !== 'patch' && trimmed !== 'minor' && trimmed !== 'major') {
151
+ return state; // Ignore invalid input
152
+ }
153
+ if (!state.target || !state.versionCheck || state.versionCheck.status !== 'needs-bump') {
154
+ return state;
155
+ }
156
+ // Compute the bumped version in the reducer (pure, synchronous)
157
+ const bumpType = trimmed;
158
+ try {
159
+ const newVersion = bumpVersion(state.versionCheck.latestVersion, bumpType);
160
+ return {
161
+ ...state,
162
+ // Move to a transitional sub-state — the effect for bump-prompt
163
+ // will see resolvedVersion is set and perform the async manifest update.
164
+ // We keep phase as 'bump-prompt' so the effect can detect the submission.
165
+ phase: 'building-plan',
166
+ resolvedVersion: newVersion,
167
+ };
168
+ }
169
+ catch {
170
+ return {
171
+ ...state,
172
+ errorMessage: 'Failed to compute bumped version.',
173
+ phase: 'error',
174
+ };
175
+ }
176
+ }
177
+ case 'SUBMIT_CONFIRM': {
178
+ const trimmed = state.confirmValue.trim().toLowerCase();
179
+ if (trimmed === 'y' || trimmed === 'yes') {
180
+ return { ...state, phase: 'building-plan' };
181
+ }
182
+ else if (trimmed === 'n' || trimmed === 'no') {
183
+ return { ...state, errorMessage: 'Publish cancelled.', phase: 'error' };
184
+ }
185
+ // Otherwise ignore (wait for valid input)
186
+ return state;
187
+ }
188
+ case 'SUBMIT_DESCRIPTION': {
189
+ const trimmed = state.descriptionValue.trim();
190
+ if (!trimmed)
191
+ return state; // Don't allow empty description
192
+ return {
193
+ ...state,
194
+ descriptionValue: trimmed,
195
+ metadataSubPhase: 'tags',
196
+ };
197
+ }
198
+ case 'SUBMIT_TAGS':
199
+ return {
200
+ ...state,
201
+ metadataSubPhase: 'building-staging',
202
+ };
203
+ case 'VALIDATE_DONE': {
204
+ if (!action.validation.valid) {
205
+ return {
206
+ ...state,
207
+ errorMessage: `Validation failed:\n${action.validation.errors.map((e) => ` - ${e}`).join('\n')}`,
208
+ phase: 'error',
209
+ validation: action.validation,
210
+ };
211
+ }
212
+ return {
213
+ ...state,
214
+ phase: 'checking-registry',
215
+ validation: action.validation,
216
+ };
217
+ }
218
+ default:
219
+ return state;
220
+ }
221
+ }
222
+ // ---------------------------------------------------------------------------
223
+ // Hook
224
+ // ---------------------------------------------------------------------------
225
+ export function usePublishState(flags) {
226
+ const [state, dispatch] = useReducer(publishReducer, undefined, () => createInitialState(flags));
227
+ // Temp directory ref for staging / --from-installed cleanup
228
+ const tempDirRef = useRef(null);
229
+ // --- Phase: resolving-installed ---
230
+ useEffect(() => {
231
+ if (state.phase !== 'resolving-installed')
232
+ return;
233
+ let cancelled = false;
234
+ const resolveInstalled = async () => {
235
+ const assetName = state.flags.rawPath;
236
+ const { name, org } = parseOrgFromName(assetName);
237
+ const projectRoot = findProjectRoot(state.flags.projectRoot);
238
+ const tools = await resolveTools(state.flags.tool);
239
+ const adapter = tools[0].adapter;
240
+ const lockfile = await readLockfile(projectRoot);
241
+ const installed = findInstalledAsset(lockfile, name, undefined, org);
242
+ if (!installed) {
243
+ if (!cancelled) {
244
+ dispatch({ message: `Asset '${assetName}' is not installed.`, type: 'ERROR' });
245
+ }
246
+ return;
247
+ }
248
+ // Create temp directory and copy installed files
249
+ const tempDir = await mkdir(join(tmpdir(), 'atk-publish-'), { recursive: true })
250
+ .then(() => join(tmpdir(), `atk-publish-${Date.now()}-${Math.random().toString(36).slice(2)}`))
251
+ .then(async (dir) => {
252
+ await mkdir(dir, { recursive: true });
253
+ return dir;
254
+ });
255
+ if (cancelled) {
256
+ await rm(tempDir, { force: true, recursive: true });
257
+ return;
258
+ }
259
+ tempDirRef.current = tempDir;
260
+ // Resolve installed paths via the adapter
261
+ const resolved = resolveInstalledPaths(installed, adapter, projectRoot);
262
+ // Copy each installed file preserving sourcePath directory structure
263
+ for (const file of resolved.files) {
264
+ const sourceAbsolute = join(projectRoot, file.installedPath);
265
+ const destPath = join(tempDir, file.sourcePath);
266
+ const destDir = dirname(destPath);
267
+ await mkdir(destDir, { recursive: true });
268
+ await cp(sourceAbsolute, destPath);
269
+ }
270
+ // Generate manifest.json in the temp dir
271
+ const manifest = {
272
+ author: 'unknown',
273
+ description: `Published from installed asset ${installed.name}`,
274
+ entrypoint: installed.files[0]?.sourcePath ?? 'index.md',
275
+ name: installed.name,
276
+ ...(installed.org ? { org: installed.org } : {}),
277
+ type: installed.type,
278
+ version: installed.version,
279
+ };
280
+ await writeFile(join(tempDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
281
+ if (!cancelled) {
282
+ dispatch({ resolvedPath: tempDir, type: 'RESOLVED_INSTALLED' });
283
+ }
284
+ };
285
+ resolveInstalled().catch((err) => {
286
+ if (!cancelled) {
287
+ dispatch({ message: err instanceof Error ? err.message : String(err), type: 'ERROR' });
288
+ }
289
+ });
290
+ return () => {
291
+ cancelled = true;
292
+ };
293
+ // Phase-gated effect: only runs when entering 'resolving-installed'
294
+ // eslint-disable-next-line react-hooks/exhaustive-deps
295
+ }, [state.phase]);
296
+ // --- Phase: detecting ---
297
+ useEffect(() => {
298
+ if (state.phase !== 'detecting')
299
+ return;
300
+ let cancelled = false;
301
+ const detect = async () => {
302
+ // First, try manifest-based detection (existing flow)
303
+ try {
304
+ const detected = await detectPublishType(state.resolvedPath);
305
+ if (cancelled)
306
+ return;
307
+ dispatch({ target: detected, type: 'DETECT_DONE' });
308
+ return;
309
+ }
310
+ catch (manifestError) {
311
+ // Only attempt raw detection if the error is specifically about missing manifest/bundle
312
+ const manifestMsg = manifestError instanceof Error ? manifestError.message : String(manifestError);
313
+ if (!manifestMsg.includes('No manifest.json or bundle.json found')) {
314
+ // This is a different error (e.g., invalid JSON in manifest) — surface it directly
315
+ if (!cancelled) {
316
+ dispatch({ message: manifestMsg, type: 'ERROR' });
317
+ }
318
+ return;
319
+ }
320
+ }
321
+ // Manifest not found — attempt raw asset detection
322
+ const resolvedTools = await resolveTools(state.flags.tool);
323
+ const adapter = resolvedTools[0].adapter;
324
+ if (cancelled)
325
+ return;
326
+ const projectRoot = findProjectRoot(state.flags.projectRoot);
327
+ const detection = await detectAssetFromPath(state.resolvedPath, adapter, projectRoot);
328
+ if (cancelled)
329
+ return;
330
+ // Pre-fill metadata
331
+ const author = (await getGitUserName()) ?? '';
332
+ if (cancelled)
333
+ return;
334
+ const lockfile = await readLockfile(projectRoot);
335
+ if (cancelled)
336
+ return;
337
+ const tools = await detectCompatibleTools(detection.assetType);
338
+ if (cancelled)
339
+ return;
340
+ const orgVal = lockfile.org ?? '';
341
+ // Non-interactive mode: both --description and --tags flags provided
342
+ if (state.flags.descriptionFlag && state.flags.tagsFlag) {
343
+ const tags = state.flags.tagsFlag
344
+ .split(',')
345
+ .map((t) => t.trim())
346
+ .filter(Boolean);
347
+ const stagingDir = await buildStagingArea(detection, {
348
+ author,
349
+ description: state.flags.descriptionFlag,
350
+ ...(lockfile.org ? { org: lockfile.org } : {}),
351
+ tags,
352
+ tools: tools.map((t) => ({ tool: t })),
353
+ });
354
+ if (cancelled)
355
+ return;
356
+ tempDirRef.current = stagingDir;
357
+ const detected = await detectPublishType(stagingDir);
358
+ if (cancelled)
359
+ return;
360
+ dispatch({ rawDetection: detection, target: detected, type: 'RAW_DETECT_DONE_NON_INTERACTIVE' });
361
+ }
362
+ else {
363
+ // Interactive mode — gather metadata from the user
364
+ dispatch({
365
+ authorValue: author,
366
+ orgValue: orgVal,
367
+ rawDetection: detection,
368
+ toolsDetected: tools.map((t) => ({ tool: t })),
369
+ type: 'RAW_DETECT_DONE',
370
+ });
371
+ }
372
+ };
373
+ detect().catch((err) => {
374
+ if (!cancelled) {
375
+ dispatch({ message: err instanceof Error ? err.message : String(err), type: 'ERROR' });
376
+ }
377
+ });
378
+ return () => {
379
+ cancelled = true;
380
+ };
381
+ // Phase-gated effect: only runs when entering 'detecting'
382
+ // eslint-disable-next-line react-hooks/exhaustive-deps
383
+ }, [state.phase]);
384
+ // --- Phase: gathering-metadata / building-staging sub-phase ---
385
+ useEffect(() => {
386
+ if (state.phase !== 'gathering-metadata' || state.metadataSubPhase !== 'building-staging')
387
+ return;
388
+ let cancelled = false;
389
+ const build = async () => {
390
+ // Parse tags from comma-separated string
391
+ const tags = state.tagsValue
392
+ .split(',')
393
+ .map((t) => t.trim())
394
+ .filter(Boolean);
395
+ // Build staging area
396
+ const stagingDir = await buildStagingArea(state.rawDetection, {
397
+ author: state.authorValue,
398
+ description: state.descriptionValue,
399
+ org: state.orgValue || undefined,
400
+ tags,
401
+ tools: state.toolsDetected,
402
+ });
403
+ if (cancelled)
404
+ return;
405
+ // Store for cleanup
406
+ tempDirRef.current = stagingDir;
407
+ // Re-detect using the staging area (now has manifest.json)
408
+ const detectedTarget = await detectPublishType(stagingDir);
409
+ if (cancelled)
410
+ return;
411
+ dispatch({ target: detectedTarget, type: 'STAGING_DONE' });
412
+ };
413
+ build().catch((err) => {
414
+ if (!cancelled) {
415
+ dispatch({ message: err instanceof Error ? err.message : String(err), type: 'ERROR' });
416
+ }
417
+ });
418
+ return () => {
419
+ cancelled = true;
420
+ };
421
+ // Phase-gated effect: only runs when entering building-staging sub-phase
422
+ // eslint-disable-next-line react-hooks/exhaustive-deps
423
+ }, [state.phase, state.metadataSubPhase]);
424
+ // --- Phase: validating ---
425
+ useEffect(() => {
426
+ if (state.phase !== 'validating')
427
+ return;
428
+ if (!state.target)
429
+ return;
430
+ let cancelled = false;
431
+ const validate = async () => {
432
+ const result = await validatePublishTarget(state.target);
433
+ if (cancelled)
434
+ return;
435
+ dispatch({ type: 'VALIDATE_DONE', validation: result });
436
+ };
437
+ validate().catch((err) => {
438
+ if (!cancelled) {
439
+ dispatch({ message: err instanceof Error ? err.message : String(err), type: 'ERROR' });
440
+ }
441
+ });
442
+ return () => {
443
+ cancelled = true;
444
+ };
445
+ // Phase-gated effect: only runs when entering 'validating'
446
+ // eslint-disable-next-line react-hooks/exhaustive-deps
447
+ }, [state.phase]);
448
+ // --- Phase: checking-registry ---
449
+ useEffect(() => {
450
+ if (state.phase !== 'checking-registry')
451
+ return;
452
+ if (!state.target)
453
+ return;
454
+ let cancelled = false;
455
+ const check = async () => {
456
+ const authToken = await getGitHubToken();
457
+ if (cancelled)
458
+ return;
459
+ const reg = await fetchRegistry({ force: state.flags.refresh });
460
+ if (cancelled)
461
+ return;
462
+ const versionResult = checkRegistryVersion(state.target, reg);
463
+ if (cancelled)
464
+ return;
465
+ let isUpdate = false;
466
+ let resolvedVersion = '';
467
+ if (versionResult.status === 'new-asset') {
468
+ isUpdate = false;
469
+ resolvedVersion = state.target.data.version;
470
+ }
471
+ else if (versionResult.status === 'version-already-bumped') {
472
+ isUpdate = true;
473
+ resolvedVersion = state.target.data.version;
474
+ }
475
+ else {
476
+ // needs-bump
477
+ isUpdate = true;
478
+ }
479
+ if (cancelled)
480
+ return;
481
+ dispatch({
482
+ isUpdate,
483
+ resolvedVersion,
484
+ token: authToken,
485
+ type: 'REGISTRY_CHECK_DONE',
486
+ versionCheck: versionResult,
487
+ });
488
+ };
489
+ check().catch((err) => {
490
+ if (!cancelled) {
491
+ dispatch({ message: mapPublishError(err), type: 'ERROR' });
492
+ }
493
+ });
494
+ return () => {
495
+ cancelled = true;
496
+ };
497
+ // Phase-gated effect: only runs when entering 'checking-registry'
498
+ // eslint-disable-next-line react-hooks/exhaustive-deps
499
+ }, [state.phase]);
500
+ // --- Phase: building-plan ---
501
+ // This effect also handles the async manifest update when coming from bump-prompt
502
+ useEffect(() => {
503
+ if (state.phase !== 'building-plan')
504
+ return;
505
+ if (!state.target)
506
+ return;
507
+ let cancelled = false;
508
+ const build = async () => {
509
+ let currentTarget = state.target;
510
+ const currentVersion = state.resolvedVersion;
511
+ // If we came from a bump submission, update the manifest file first
512
+ if (state.versionCheck?.status === 'needs-bump' && currentVersion) {
513
+ const manifestFile = currentTarget.type === 'asset' ? 'manifest.json' : 'bundle.json';
514
+ await updateManifestVersion(currentTarget.sourceDir, manifestFile, currentVersion);
515
+ if (cancelled)
516
+ return;
517
+ // Re-read the target so it has the updated version
518
+ currentTarget = await detectPublishType(currentTarget.sourceDir);
519
+ if (cancelled)
520
+ return;
521
+ }
522
+ const publishPlan = await buildPublishPlan(currentTarget, currentVersion, state.isUpdate, state.flags.message);
523
+ if (cancelled)
524
+ return;
525
+ dispatch({ plan: publishPlan, type: 'PLAN_DONE' });
526
+ };
527
+ build().catch((err) => {
528
+ if (!cancelled) {
529
+ dispatch({ message: err instanceof Error ? err.message : String(err), type: 'ERROR' });
530
+ }
531
+ });
532
+ return () => {
533
+ cancelled = true;
534
+ };
535
+ // Phase-gated effect: only runs when entering 'building-plan'
536
+ // eslint-disable-next-line react-hooks/exhaustive-deps
537
+ }, [state.phase]);
538
+ // --- Phase: publishing ---
539
+ useEffect(() => {
540
+ if (state.phase !== 'publishing')
541
+ return;
542
+ if (!state.plan || !state.token)
543
+ return;
544
+ let cancelled = false;
545
+ const publish = async () => {
546
+ const progressCallback = (msg) => {
547
+ if (!cancelled) {
548
+ dispatch({ message: msg, type: 'PUBLISH_PROGRESS' });
549
+ }
550
+ };
551
+ const result = isOrgAsset(state.plan)
552
+ ? await executeDirectPublish(state.plan, state.token, progressCallback)
553
+ : await executePublish(state.plan, state.token, progressCallback);
554
+ if (cancelled)
555
+ return;
556
+ // Clear registry cache so subsequent commands see the new version
557
+ try {
558
+ await clearCache();
559
+ }
560
+ catch {
561
+ // Best-effort — don't fail publish over cache
562
+ }
563
+ // Update lockfile in-place when publishing from an installed asset
564
+ if (state.flags.fromInstalled) {
565
+ try {
566
+ const projectRoot = findProjectRoot(state.flags.projectRoot);
567
+ const pubTools = await resolveTools(state.flags.tool);
568
+ const pubAdapter = pubTools[0].adapter;
569
+ await withLockfileLock(projectRoot, async () => {
570
+ const lockfile = await readLockfile(projectRoot);
571
+ const { name, org } = parseOrgFromName(state.flags.rawPath);
572
+ const installed = findInstalledAsset(lockfile, name, undefined, org);
573
+ if (installed) {
574
+ const resolved = resolveInstalledPaths(installed, pubAdapter, projectRoot);
575
+ const updatedFiles = await Promise.all(resolved.files.map(async (file) => ({
576
+ checksum: await hashFile(join(projectRoot, file.installedPath)),
577
+ sourcePath: file.sourcePath,
578
+ })));
579
+ const updatedLockfile = addAssetToLockfile(lockfile, {
580
+ ...installed,
581
+ files: updatedFiles,
582
+ version: state.resolvedVersion,
583
+ });
584
+ await writeLockfile(projectRoot, updatedLockfile);
585
+ }
586
+ });
587
+ }
588
+ catch {
589
+ // Best-effort — don't fail publish over lockfile update
590
+ }
591
+ }
592
+ if (cancelled)
593
+ return;
594
+ dispatch({ result, type: 'PUBLISH_DONE' });
595
+ };
596
+ publish().catch((err) => {
597
+ if (!cancelled) {
598
+ dispatch({ message: mapPublishError(err), type: 'ERROR' });
599
+ }
600
+ });
601
+ return () => {
602
+ cancelled = true;
603
+ };
604
+ // Phase-gated effect: only runs when entering 'publishing'
605
+ // eslint-disable-next-line react-hooks/exhaustive-deps
606
+ }, [state.phase]);
607
+ // --- Phase: installing (auto-install raw publish to lockfile) ---
608
+ useEffect(() => {
609
+ if (state.phase !== 'installing')
610
+ return;
611
+ let cancelled = false;
612
+ const install = async () => {
613
+ try {
614
+ const projectRoot = findProjectRoot(state.flags.projectRoot);
615
+ await withLockfileLock(projectRoot, async () => {
616
+ // Read current lockfile
617
+ const lockfile = await readLockfile(projectRoot);
618
+ // Compute checksums from actual project files (not staging copies)
619
+ const files = [];
620
+ for (const file of state.rawDetection.files) {
621
+ const checksum = await hashFile(file.absolutePath);
622
+ files.push({
623
+ checksum,
624
+ sourcePath: file.relativePath,
625
+ });
626
+ }
627
+ // Build InstalledAsset entry
628
+ const installedAsset = {
629
+ files,
630
+ installedAt: new Date().toISOString(),
631
+ installReason: 'direct',
632
+ name: state.rawDetection.name,
633
+ org: state.orgValue || undefined,
634
+ type: state.rawDetection.assetType,
635
+ version: state.resolvedVersion || state.target?.data.version || '1.0.0',
636
+ };
637
+ // Add to lockfile and write
638
+ const updatedLockfile = addAssetToLockfile(lockfile, installedAsset);
639
+ await writeLockfile(projectRoot, updatedLockfile);
640
+ });
641
+ if (!cancelled) {
642
+ dispatch({ type: 'INSTALL_DONE' });
643
+ }
644
+ }
645
+ catch {
646
+ // Publish already succeeded, so just warn and move to done
647
+ if (!cancelled) {
648
+ dispatch({ type: 'INSTALL_DONE' });
649
+ }
650
+ }
651
+ };
652
+ install();
653
+ return () => {
654
+ cancelled = true;
655
+ };
656
+ // Phase-gated effect: only runs when entering 'installing'
657
+ // eslint-disable-next-line react-hooks/exhaustive-deps
658
+ }, [state.phase]);
659
+ // --- Cleanup temp directory on error or done ---
660
+ useEffect(() => {
661
+ if (state.phase !== 'error' && state.phase !== 'done' && state.phase !== 'dry-run-result')
662
+ return;
663
+ if (tempDirRef.current) {
664
+ const dir = tempDirRef.current;
665
+ tempDirRef.current = null;
666
+ rm(dir, { force: true, recursive: true }).catch(() => {
667
+ // Best-effort cleanup — ignore errors
668
+ });
669
+ }
670
+ }, [state.phase]);
671
+ return [state, dispatch];
672
+ }
673
+ //# sourceMappingURL=usePublishState.js.map