@detergent-software/atk 3.0.0-dev.3 → 3.0.0-dev.4

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 (36) hide show
  1. package/build/commands/browse.d.ts.map +1 -1
  2. package/build/commands/browse.js +3 -80
  3. package/build/commands/browse.js.map +1 -1
  4. package/build/commands/prune.d.ts.map +1 -1
  5. package/build/commands/prune.js +14 -32
  6. package/build/commands/prune.js.map +1 -1
  7. package/build/commands/publish.d.ts.map +1 -1
  8. package/build/commands/publish.js +46 -583
  9. package/build/commands/publish.js.map +1 -1
  10. package/build/commands/setup.d.ts.map +1 -1
  11. package/build/commands/setup.js +37 -238
  12. package/build/commands/setup.js.map +1 -1
  13. package/build/commands/uninstall.d.ts.map +1 -1
  14. package/build/commands/uninstall.js +93 -211
  15. package/build/commands/uninstall.js.map +1 -1
  16. package/build/hooks/useBrowseState.d.ts +23 -0
  17. package/build/hooks/useBrowseState.d.ts.map +1 -1
  18. package/build/hooks/useBrowseState.js +77 -1
  19. package/build/hooks/useBrowseState.js.map +1 -1
  20. package/build/hooks/useConfirmation.d.ts +18 -0
  21. package/build/hooks/useConfirmation.d.ts.map +1 -0
  22. package/build/hooks/useConfirmation.js +56 -0
  23. package/build/hooks/useConfirmation.js.map +1 -0
  24. package/build/hooks/usePublishState.d.ts +115 -0
  25. package/build/hooks/usePublishState.d.ts.map +1 -0
  26. package/build/hooks/usePublishState.js +667 -0
  27. package/build/hooks/usePublishState.js.map +1 -0
  28. package/build/hooks/useSetupState.d.ts +57 -0
  29. package/build/hooks/useSetupState.d.ts.map +1 -0
  30. package/build/hooks/useSetupState.js +283 -0
  31. package/build/hooks/useSetupState.js.map +1 -0
  32. package/build/hooks/useUninstallState.d.ts +91 -0
  33. package/build/hooks/useUninstallState.d.ts.map +1 -0
  34. package/build/hooks/useUninstallState.js +322 -0
  35. package/build/hooks/useUninstallState.js.map +1 -0
  36. package/package.json +1 -1
@@ -0,0 +1,667 @@
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 { resolveTool } 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 { adapter } = await resolveTool(state.flags.tool);
239
+ const lockfile = await readLockfile(projectRoot);
240
+ const installed = findInstalledAsset(lockfile, name, undefined, org);
241
+ if (!installed) {
242
+ if (!cancelled) {
243
+ dispatch({ message: `Asset '${assetName}' is not installed.`, type: 'ERROR' });
244
+ }
245
+ return;
246
+ }
247
+ // Create temp directory and copy installed files
248
+ const tempDir = await mkdir(join(tmpdir(), 'atk-publish-'), { recursive: true })
249
+ .then(() => join(tmpdir(), `atk-publish-${Date.now()}-${Math.random().toString(36).slice(2)}`))
250
+ .then(async (dir) => {
251
+ await mkdir(dir, { recursive: true });
252
+ return dir;
253
+ });
254
+ if (cancelled) {
255
+ await rm(tempDir, { force: true, recursive: true });
256
+ return;
257
+ }
258
+ tempDirRef.current = tempDir;
259
+ // Resolve installed paths via the adapter
260
+ const resolved = resolveInstalledPaths(installed, adapter, projectRoot);
261
+ // Copy each installed file preserving sourcePath directory structure
262
+ for (const file of resolved.files) {
263
+ const sourceAbsolute = join(projectRoot, file.installedPath);
264
+ const destPath = join(tempDir, file.sourcePath);
265
+ const destDir = dirname(destPath);
266
+ await mkdir(destDir, { recursive: true });
267
+ await cp(sourceAbsolute, destPath);
268
+ }
269
+ // Generate manifest.json in the temp dir
270
+ const manifest = {
271
+ author: 'unknown',
272
+ description: `Published from installed asset ${installed.name}`,
273
+ entrypoint: installed.files[0]?.sourcePath ?? 'index.md',
274
+ name: installed.name,
275
+ ...(installed.org ? { org: installed.org } : {}),
276
+ type: installed.type,
277
+ version: installed.version,
278
+ };
279
+ await writeFile(join(tempDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
280
+ if (!cancelled) {
281
+ dispatch({ resolvedPath: tempDir, type: 'RESOLVED_INSTALLED' });
282
+ }
283
+ };
284
+ resolveInstalled().catch((err) => {
285
+ if (!cancelled) {
286
+ dispatch({ message: err instanceof Error ? err.message : String(err), type: 'ERROR' });
287
+ }
288
+ });
289
+ return () => {
290
+ cancelled = true;
291
+ };
292
+ // Phase-gated effect: only runs when entering 'resolving-installed'
293
+ // eslint-disable-next-line react-hooks/exhaustive-deps
294
+ }, [state.phase]);
295
+ // --- Phase: detecting ---
296
+ useEffect(() => {
297
+ if (state.phase !== 'detecting')
298
+ return;
299
+ let cancelled = false;
300
+ const detect = async () => {
301
+ // First, try manifest-based detection (existing flow)
302
+ try {
303
+ const detected = await detectPublishType(state.resolvedPath);
304
+ if (cancelled)
305
+ return;
306
+ dispatch({ target: detected, type: 'DETECT_DONE' });
307
+ return;
308
+ }
309
+ catch (manifestError) {
310
+ // Only attempt raw detection if the error is specifically about missing manifest/bundle
311
+ const manifestMsg = manifestError instanceof Error ? manifestError.message : String(manifestError);
312
+ if (!manifestMsg.includes('No manifest.json or bundle.json found')) {
313
+ // This is a different error (e.g., invalid JSON in manifest) — surface it directly
314
+ if (!cancelled) {
315
+ dispatch({ message: manifestMsg, type: 'ERROR' });
316
+ }
317
+ return;
318
+ }
319
+ }
320
+ // Manifest not found — attempt raw asset detection
321
+ const { adapter } = await resolveTool(state.flags.tool);
322
+ if (cancelled)
323
+ return;
324
+ const projectRoot = findProjectRoot(state.flags.projectRoot);
325
+ const detection = await detectAssetFromPath(state.resolvedPath, adapter, projectRoot);
326
+ if (cancelled)
327
+ return;
328
+ // Pre-fill metadata
329
+ const author = (await getGitUserName()) ?? '';
330
+ if (cancelled)
331
+ return;
332
+ const lockfile = await readLockfile(projectRoot);
333
+ if (cancelled)
334
+ return;
335
+ const tools = await detectCompatibleTools(detection.assetType);
336
+ if (cancelled)
337
+ return;
338
+ const orgVal = lockfile.org ?? '';
339
+ // Non-interactive mode: both --description and --tags flags provided
340
+ if (state.flags.descriptionFlag && state.flags.tagsFlag) {
341
+ const tags = state.flags.tagsFlag.split(',').map((t) => t.trim()).filter(Boolean);
342
+ const stagingDir = await buildStagingArea(detection, {
343
+ author,
344
+ description: state.flags.descriptionFlag,
345
+ ...(lockfile.org ? { org: lockfile.org } : {}),
346
+ tags,
347
+ tools: tools.map((t) => ({ tool: t })),
348
+ });
349
+ if (cancelled)
350
+ return;
351
+ tempDirRef.current = stagingDir;
352
+ const detected = await detectPublishType(stagingDir);
353
+ if (cancelled)
354
+ return;
355
+ dispatch({ rawDetection: detection, target: detected, type: 'RAW_DETECT_DONE_NON_INTERACTIVE' });
356
+ }
357
+ else {
358
+ // Interactive mode — gather metadata from the user
359
+ dispatch({
360
+ authorValue: author,
361
+ orgValue: orgVal,
362
+ rawDetection: detection,
363
+ toolsDetected: tools.map((t) => ({ tool: t })),
364
+ type: 'RAW_DETECT_DONE',
365
+ });
366
+ }
367
+ };
368
+ detect().catch((err) => {
369
+ if (!cancelled) {
370
+ dispatch({ message: err instanceof Error ? err.message : String(err), type: 'ERROR' });
371
+ }
372
+ });
373
+ return () => {
374
+ cancelled = true;
375
+ };
376
+ // Phase-gated effect: only runs when entering 'detecting'
377
+ // eslint-disable-next-line react-hooks/exhaustive-deps
378
+ }, [state.phase]);
379
+ // --- Phase: gathering-metadata / building-staging sub-phase ---
380
+ useEffect(() => {
381
+ if (state.phase !== 'gathering-metadata' || state.metadataSubPhase !== 'building-staging')
382
+ return;
383
+ let cancelled = false;
384
+ const build = async () => {
385
+ // Parse tags from comma-separated string
386
+ const tags = state.tagsValue
387
+ .split(',')
388
+ .map((t) => t.trim())
389
+ .filter(Boolean);
390
+ // Build staging area
391
+ const stagingDir = await buildStagingArea(state.rawDetection, {
392
+ author: state.authorValue,
393
+ description: state.descriptionValue,
394
+ org: state.orgValue || undefined,
395
+ tags,
396
+ tools: state.toolsDetected,
397
+ });
398
+ if (cancelled)
399
+ return;
400
+ // Store for cleanup
401
+ tempDirRef.current = stagingDir;
402
+ // Re-detect using the staging area (now has manifest.json)
403
+ const detectedTarget = await detectPublishType(stagingDir);
404
+ if (cancelled)
405
+ return;
406
+ dispatch({ target: detectedTarget, type: 'STAGING_DONE' });
407
+ };
408
+ build().catch((err) => {
409
+ if (!cancelled) {
410
+ dispatch({ message: err instanceof Error ? err.message : String(err), type: 'ERROR' });
411
+ }
412
+ });
413
+ return () => {
414
+ cancelled = true;
415
+ };
416
+ // Phase-gated effect: only runs when entering building-staging sub-phase
417
+ // eslint-disable-next-line react-hooks/exhaustive-deps
418
+ }, [state.phase, state.metadataSubPhase]);
419
+ // --- Phase: validating ---
420
+ useEffect(() => {
421
+ if (state.phase !== 'validating')
422
+ return;
423
+ if (!state.target)
424
+ return;
425
+ let cancelled = false;
426
+ const validate = async () => {
427
+ const result = await validatePublishTarget(state.target);
428
+ if (cancelled)
429
+ return;
430
+ dispatch({ type: 'VALIDATE_DONE', validation: result });
431
+ };
432
+ validate().catch((err) => {
433
+ if (!cancelled) {
434
+ dispatch({ message: err instanceof Error ? err.message : String(err), type: 'ERROR' });
435
+ }
436
+ });
437
+ return () => {
438
+ cancelled = true;
439
+ };
440
+ // Phase-gated effect: only runs when entering 'validating'
441
+ // eslint-disable-next-line react-hooks/exhaustive-deps
442
+ }, [state.phase]);
443
+ // --- Phase: checking-registry ---
444
+ useEffect(() => {
445
+ if (state.phase !== 'checking-registry')
446
+ return;
447
+ if (!state.target)
448
+ return;
449
+ let cancelled = false;
450
+ const check = async () => {
451
+ const authToken = await getGitHubToken();
452
+ if (cancelled)
453
+ return;
454
+ const reg = await fetchRegistry({ force: state.flags.refresh });
455
+ if (cancelled)
456
+ return;
457
+ const versionResult = checkRegistryVersion(state.target, reg);
458
+ if (cancelled)
459
+ return;
460
+ let isUpdate = false;
461
+ let resolvedVersion = '';
462
+ if (versionResult.status === 'new-asset') {
463
+ isUpdate = false;
464
+ resolvedVersion = state.target.data.version;
465
+ }
466
+ else if (versionResult.status === 'version-already-bumped') {
467
+ isUpdate = true;
468
+ resolvedVersion = state.target.data.version;
469
+ }
470
+ else {
471
+ // needs-bump
472
+ isUpdate = true;
473
+ }
474
+ if (cancelled)
475
+ return;
476
+ dispatch({
477
+ isUpdate,
478
+ resolvedVersion,
479
+ token: authToken,
480
+ type: 'REGISTRY_CHECK_DONE',
481
+ versionCheck: versionResult,
482
+ });
483
+ };
484
+ check().catch((err) => {
485
+ if (!cancelled) {
486
+ dispatch({ message: mapPublishError(err), type: 'ERROR' });
487
+ }
488
+ });
489
+ return () => {
490
+ cancelled = true;
491
+ };
492
+ // Phase-gated effect: only runs when entering 'checking-registry'
493
+ // eslint-disable-next-line react-hooks/exhaustive-deps
494
+ }, [state.phase]);
495
+ // --- Phase: building-plan ---
496
+ // This effect also handles the async manifest update when coming from bump-prompt
497
+ useEffect(() => {
498
+ if (state.phase !== 'building-plan')
499
+ return;
500
+ if (!state.target)
501
+ return;
502
+ let cancelled = false;
503
+ const build = async () => {
504
+ let currentTarget = state.target;
505
+ const currentVersion = state.resolvedVersion;
506
+ // If we came from a bump submission, update the manifest file first
507
+ if (state.versionCheck?.status === 'needs-bump' && currentVersion) {
508
+ const manifestFile = currentTarget.type === 'asset' ? 'manifest.json' : 'bundle.json';
509
+ await updateManifestVersion(currentTarget.sourceDir, manifestFile, currentVersion);
510
+ if (cancelled)
511
+ return;
512
+ // Re-read the target so it has the updated version
513
+ currentTarget = await detectPublishType(currentTarget.sourceDir);
514
+ if (cancelled)
515
+ return;
516
+ }
517
+ const publishPlan = await buildPublishPlan(currentTarget, currentVersion, state.isUpdate, state.flags.message);
518
+ if (cancelled)
519
+ return;
520
+ dispatch({ plan: publishPlan, type: 'PLAN_DONE' });
521
+ };
522
+ build().catch((err) => {
523
+ if (!cancelled) {
524
+ dispatch({ message: err instanceof Error ? err.message : String(err), type: 'ERROR' });
525
+ }
526
+ });
527
+ return () => {
528
+ cancelled = true;
529
+ };
530
+ // Phase-gated effect: only runs when entering 'building-plan'
531
+ // eslint-disable-next-line react-hooks/exhaustive-deps
532
+ }, [state.phase]);
533
+ // --- Phase: publishing ---
534
+ useEffect(() => {
535
+ if (state.phase !== 'publishing')
536
+ return;
537
+ if (!state.plan || !state.token)
538
+ return;
539
+ let cancelled = false;
540
+ const publish = async () => {
541
+ const progressCallback = (msg) => {
542
+ if (!cancelled) {
543
+ dispatch({ message: msg, type: 'PUBLISH_PROGRESS' });
544
+ }
545
+ };
546
+ const result = isOrgAsset(state.plan)
547
+ ? await executeDirectPublish(state.plan, state.token, progressCallback)
548
+ : await executePublish(state.plan, state.token, progressCallback);
549
+ if (cancelled)
550
+ return;
551
+ // Clear registry cache so subsequent commands see the new version
552
+ try {
553
+ await clearCache();
554
+ }
555
+ catch {
556
+ // Best-effort — don't fail publish over cache
557
+ }
558
+ // Update lockfile in-place when publishing from an installed asset
559
+ if (state.flags.fromInstalled) {
560
+ try {
561
+ const projectRoot = findProjectRoot(state.flags.projectRoot);
562
+ const { adapter: pubAdapter } = await resolveTool(state.flags.tool);
563
+ await withLockfileLock(projectRoot, async () => {
564
+ const lockfile = await readLockfile(projectRoot);
565
+ const { name, org } = parseOrgFromName(state.flags.rawPath);
566
+ const installed = findInstalledAsset(lockfile, name, undefined, org);
567
+ if (installed) {
568
+ const resolved = resolveInstalledPaths(installed, pubAdapter, projectRoot);
569
+ const updatedFiles = await Promise.all(resolved.files.map(async (file) => ({
570
+ checksum: await hashFile(join(projectRoot, file.installedPath)),
571
+ sourcePath: file.sourcePath,
572
+ })));
573
+ const updatedLockfile = addAssetToLockfile(lockfile, {
574
+ ...installed,
575
+ files: updatedFiles,
576
+ version: state.resolvedVersion,
577
+ });
578
+ await writeLockfile(projectRoot, updatedLockfile);
579
+ }
580
+ });
581
+ }
582
+ catch {
583
+ // Best-effort — don't fail publish over lockfile update
584
+ }
585
+ }
586
+ if (cancelled)
587
+ return;
588
+ dispatch({ result, type: 'PUBLISH_DONE' });
589
+ };
590
+ publish().catch((err) => {
591
+ if (!cancelled) {
592
+ dispatch({ message: mapPublishError(err), type: 'ERROR' });
593
+ }
594
+ });
595
+ return () => {
596
+ cancelled = true;
597
+ };
598
+ // Phase-gated effect: only runs when entering 'publishing'
599
+ // eslint-disable-next-line react-hooks/exhaustive-deps
600
+ }, [state.phase]);
601
+ // --- Phase: installing (auto-install raw publish to lockfile) ---
602
+ useEffect(() => {
603
+ if (state.phase !== 'installing')
604
+ return;
605
+ let cancelled = false;
606
+ const install = async () => {
607
+ try {
608
+ const projectRoot = findProjectRoot(state.flags.projectRoot);
609
+ await withLockfileLock(projectRoot, async () => {
610
+ // Read current lockfile
611
+ const lockfile = await readLockfile(projectRoot);
612
+ // Compute checksums from actual project files (not staging copies)
613
+ const files = [];
614
+ for (const file of state.rawDetection.files) {
615
+ const checksum = await hashFile(file.absolutePath);
616
+ files.push({
617
+ checksum,
618
+ sourcePath: file.relativePath,
619
+ });
620
+ }
621
+ // Build InstalledAsset entry
622
+ const installedAsset = {
623
+ files,
624
+ installedAt: new Date().toISOString(),
625
+ installReason: 'direct',
626
+ name: state.rawDetection.name,
627
+ org: state.orgValue || undefined,
628
+ type: state.rawDetection.assetType,
629
+ version: state.resolvedVersion || state.target?.data.version || '1.0.0',
630
+ };
631
+ // Add to lockfile and write
632
+ const updatedLockfile = addAssetToLockfile(lockfile, installedAsset);
633
+ await writeLockfile(projectRoot, updatedLockfile);
634
+ });
635
+ if (!cancelled) {
636
+ dispatch({ type: 'INSTALL_DONE' });
637
+ }
638
+ }
639
+ catch {
640
+ // Publish already succeeded, so just warn and move to done
641
+ if (!cancelled) {
642
+ dispatch({ type: 'INSTALL_DONE' });
643
+ }
644
+ }
645
+ };
646
+ install();
647
+ return () => {
648
+ cancelled = true;
649
+ };
650
+ // Phase-gated effect: only runs when entering 'installing'
651
+ // eslint-disable-next-line react-hooks/exhaustive-deps
652
+ }, [state.phase]);
653
+ // --- Cleanup temp directory on error or done ---
654
+ useEffect(() => {
655
+ if (state.phase !== 'error' && state.phase !== 'done' && state.phase !== 'dry-run-result')
656
+ return;
657
+ if (tempDirRef.current) {
658
+ const dir = tempDirRef.current;
659
+ tempDirRef.current = null;
660
+ rm(dir, { force: true, recursive: true }).catch(() => {
661
+ // Best-effort cleanup — ignore errors
662
+ });
663
+ }
664
+ }, [state.phase]);
665
+ return [state, dispatch];
666
+ }
667
+ //# sourceMappingURL=usePublishState.js.map