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

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 (106) hide show
  1. package/build/commands/audit.d.ts.map +1 -1
  2. package/build/commands/audit.js +15 -10
  3. package/build/commands/audit.js.map +1 -1
  4. package/build/commands/browse.d.ts.map +1 -1
  5. package/build/commands/browse.js +39 -114
  6. package/build/commands/browse.js.map +1 -1
  7. package/build/commands/diff.d.ts.map +1 -1
  8. package/build/commands/diff.js +4 -2
  9. package/build/commands/diff.js.map +1 -1
  10. package/build/commands/info.d.ts.map +1 -1
  11. package/build/commands/info.js +15 -2
  12. package/build/commands/info.js.map +1 -1
  13. package/build/commands/install.d.ts.map +1 -1
  14. package/build/commands/install.js +47 -32
  15. package/build/commands/install.js.map +1 -1
  16. package/build/commands/list.d.ts.map +1 -1
  17. package/build/commands/list.js +18 -8
  18. package/build/commands/list.js.map +1 -1
  19. package/build/commands/outdated.d.ts.map +1 -1
  20. package/build/commands/outdated.js +7 -6
  21. package/build/commands/outdated.js.map +1 -1
  22. package/build/commands/prune.d.ts.map +1 -1
  23. package/build/commands/prune.js +36 -47
  24. package/build/commands/prune.js.map +1 -1
  25. package/build/commands/publish.d.ts.map +1 -1
  26. package/build/commands/publish.js +46 -583
  27. package/build/commands/publish.js.map +1 -1
  28. package/build/commands/setup.d.ts +1 -1
  29. package/build/commands/setup.d.ts.map +1 -1
  30. package/build/commands/setup.js +49 -242
  31. package/build/commands/setup.js.map +1 -1
  32. package/build/commands/sync.d.ts.map +1 -1
  33. package/build/commands/sync.js +111 -120
  34. package/build/commands/sync.js.map +1 -1
  35. package/build/commands/uninstall.d.ts.map +1 -1
  36. package/build/commands/uninstall.js +103 -218
  37. package/build/commands/uninstall.js.map +1 -1
  38. package/build/commands/update.d.ts.map +1 -1
  39. package/build/commands/update.js +33 -23
  40. package/build/commands/update.js.map +1 -1
  41. package/build/components/AssetTable.d.ts +2 -1
  42. package/build/components/AssetTable.d.ts.map +1 -1
  43. package/build/components/AssetTable.js +5 -2
  44. package/build/components/AssetTable.js.map +1 -1
  45. package/build/components/DryRunBanner.d.ts +3 -3
  46. package/build/components/DryRunBanner.d.ts.map +1 -1
  47. package/build/components/DryRunBanner.js +2 -2
  48. package/build/components/DryRunBanner.js.map +1 -1
  49. package/build/components/InstallSummary.d.ts +3 -3
  50. package/build/components/InstallSummary.d.ts.map +1 -1
  51. package/build/components/InstallSummary.js +2 -2
  52. package/build/components/InstallSummary.js.map +1 -1
  53. package/build/hooks/useBrowseState.d.ts +23 -0
  54. package/build/hooks/useBrowseState.d.ts.map +1 -1
  55. package/build/hooks/useBrowseState.js +77 -1
  56. package/build/hooks/useBrowseState.js.map +1 -1
  57. package/build/hooks/useConfirmation.d.ts +18 -0
  58. package/build/hooks/useConfirmation.d.ts.map +1 -0
  59. package/build/hooks/useConfirmation.js +56 -0
  60. package/build/hooks/useConfirmation.js.map +1 -0
  61. package/build/hooks/useInitState.d.ts.map +1 -1
  62. package/build/hooks/useInitState.js +3 -2
  63. package/build/hooks/useInitState.js.map +1 -1
  64. package/build/hooks/usePublishState.d.ts +115 -0
  65. package/build/hooks/usePublishState.d.ts.map +1 -0
  66. package/build/hooks/usePublishState.js +670 -0
  67. package/build/hooks/usePublishState.js.map +1 -0
  68. package/build/hooks/useSetupState.d.ts +59 -0
  69. package/build/hooks/useSetupState.d.ts.map +1 -0
  70. package/build/hooks/useSetupState.js +297 -0
  71. package/build/hooks/useSetupState.js.map +1 -0
  72. package/build/hooks/useUninstallState.d.ts +102 -0
  73. package/build/hooks/useUninstallState.d.ts.map +1 -0
  74. package/build/hooks/useUninstallState.js +335 -0
  75. package/build/hooks/useUninstallState.js.map +1 -0
  76. package/build/lib/config.d.ts +4 -1
  77. package/build/lib/config.d.ts.map +1 -1
  78. package/build/lib/config.js +9 -9
  79. package/build/lib/config.js.map +1 -1
  80. package/build/lib/diagnostics.js +1 -1
  81. package/build/lib/diagnostics.js.map +1 -1
  82. package/build/lib/installer.js +1 -0
  83. package/build/lib/installer.js.map +1 -1
  84. package/build/lib/lockfile.d.ts +13 -13
  85. package/build/lib/lockfile.d.ts.map +1 -1
  86. package/build/lib/lockfile.js +39 -40
  87. package/build/lib/lockfile.js.map +1 -1
  88. package/build/lib/schemas/config.d.ts +7 -3
  89. package/build/lib/schemas/config.d.ts.map +1 -1
  90. package/build/lib/schemas/config.js +12 -2
  91. package/build/lib/schemas/config.js.map +1 -1
  92. package/build/lib/schemas/lockfile.d.ts +2 -0
  93. package/build/lib/schemas/lockfile.d.ts.map +1 -1
  94. package/build/lib/schemas/lockfile.js +1 -0
  95. package/build/lib/schemas/lockfile.js.map +1 -1
  96. package/build/lib/tool-resolver.d.ts +17 -6
  97. package/build/lib/tool-resolver.d.ts.map +1 -1
  98. package/build/lib/tool-resolver.js +31 -15
  99. package/build/lib/tool-resolver.js.map +1 -1
  100. package/build/lib/uninstaller.d.ts.map +1 -1
  101. package/build/lib/uninstaller.js +5 -2
  102. package/build/lib/uninstaller.js.map +1 -1
  103. package/build/lib/updater.d.ts.map +1 -1
  104. package/build/lib/updater.js +5 -1
  105. package/build/lib/updater.js.map +1 -1
  106. package/package.json +1 -1
@@ -1,25 +1,14 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text, useApp } from 'ink';
3
3
  import TextInput from 'ink-text-input';
4
- import { cp, mkdir, rm, writeFile } from 'node:fs/promises';
5
- import { tmpdir } from 'node:os';
6
- import { dirname, join, resolve } from 'node:path';
7
4
  import { option } from 'pastel';
8
- import { useCallback, useEffect, useRef, useState } from 'react';
5
+ import { useEffect } from 'react';
9
6
  import { z } from 'zod';
10
7
  import { DryRunBanner, Spinner } from '../components/index.js';
11
8
  import { CheckIcon } from '../components/StatusBadge.js';
12
- import { resolveInstalledPaths } from '../lib/adapter.js';
13
- import { getGitHubToken } from '../lib/auth.js';
14
- import { hashFile } from '../lib/checksum.js';
9
+ import { usePublishState } from '../hooks/usePublishState.js';
15
10
  import { DEFAULT_REGISTRY_BRANCH } from '../lib/github.js';
16
- import { detectCompatibleTools, getGitUserName } from '../lib/init.js';
17
- import { addAssetToLockfile, findInstalledAsset, readLockfile, withLockfileLock, writeLockfile, } from '../lib/lockfile.js';
18
- import { parseOrgFromName } from '../lib/org.js';
19
- import { findProjectRoot } from '../lib/paths.js';
20
- import { buildPublishPlan, buildStagingArea, bumpVersion, checkRegistryVersion, detectAssetFromPath, detectPublishType, executeDirectPublish, executePublish, isOrgAsset, mapPublishError, updateManifestVersion, validatePublishTarget, } from '../lib/publisher.js';
21
- import { clearCache, fetchRegistry } from '../lib/registry.js';
22
- import { resolveTool } from '../lib/tool-resolver.js';
11
+ import { isOrgAsset } from '../lib/publisher.js';
23
12
  export const description = 'Publish an asset or bundle to the registry';
24
13
  export const options = z.object({
25
14
  description: z
@@ -61,550 +50,20 @@ export const args = z.tuple([
61
50
  ]);
62
51
  export default function Publish({ args: [rawPath], options: { description: descriptionFlag, dryRun, fromInstalled, message, tags: tagsFlag }, }) {
63
52
  const { exit } = useApp();
64
- // Phase state machine
65
- const [phase, setPhase] = useState(fromInstalled ? 'resolving-installed' : 'detecting');
66
- // Data accumulated across phases
67
- const [target, setTarget] = useState();
68
- const [validation, setValidation] = useState();
69
- const [versionCheck, setVersionCheck] = useState();
70
- const [token, setToken] = useState();
71
- const [isUpdate, setIsUpdate] = useState(false);
72
- const [resolvedVersion, setResolvedVersion] = useState('');
73
- const [plan, setPlan] = useState();
74
- const [publishResult, setPublishResult] = useState();
75
- const [progressMessage, setProgressMessage] = useState('');
76
- // Interactive prompt values
77
- const [confirmValue, setConfirmValue] = useState('');
78
- const [bumpSelection, setBumpSelection] = useState('');
79
- // Raw publish state
80
- const [rawDetection, setRawDetection] = useState(null);
81
- const [descriptionValue, setDescriptionValue] = useState('');
82
- const [tagsValue, setTagsValue] = useState('');
83
- const [authorValue, setAuthorValue] = useState('');
84
- const [orgValue, setOrgValue] = useState('');
85
- const [toolsDetected, setToolsDetected] = useState([]);
86
- const [isRawPublish, setIsRawPublish] = useState(false);
87
- const [metadataSubPhase, setMetadataSubPhase] = useState('description');
88
- // Error and status
89
- const [errorMessage, setErrorMessage] = useState('');
90
- // Temp directory ref for --from-installed cleanup
91
- const tempDirRef = useRef(null);
92
- // When --from-installed is used, resolvedPath is set after resolving the installed asset
93
- const [resolvedPath, setResolvedPath] = useState(fromInstalled ? '' : resolve(rawPath));
94
- // ---------------------------------------------------------------------------
95
- // Cleanup helper for temp directory
96
- // ---------------------------------------------------------------------------
97
- const cleanupTempDir = useCallback(async () => {
98
- if (tempDirRef.current) {
99
- try {
100
- await rm(tempDirRef.current, { force: true, recursive: true });
101
- }
102
- catch {
103
- // Best-effort cleanup — ignore errors
104
- }
105
- tempDirRef.current = null;
106
- }
107
- }, []);
108
- // ---------------------------------------------------------------------------
109
- // Phase: Resolving installed asset (--from-installed)
110
- // ---------------------------------------------------------------------------
111
- useEffect(() => {
112
- if (phase !== 'resolving-installed')
113
- return;
114
- let cancelled = false;
115
- const resolveInstalled = async () => {
116
- const assetName = rawPath;
117
- const { name, org } = parseOrgFromName(assetName);
118
- const projectRoot = findProjectRoot();
119
- const { adapter } = await resolveTool(undefined);
120
- const lockfile = await readLockfile(projectRoot);
121
- const installed = findInstalledAsset(lockfile, name, undefined, org);
122
- if (!installed) {
123
- if (!cancelled) {
124
- setErrorMessage(`Asset '${assetName}' is not installed.`);
125
- setPhase('error');
126
- }
127
- return;
128
- }
129
- // Create temp directory and copy installed files
130
- const tempDir = await mkdir(join(tmpdir(), 'atk-publish-'), { recursive: true })
131
- .then(() => join(tmpdir(), `atk-publish-${Date.now()}-${Math.random().toString(36).slice(2)}`))
132
- .then(async (dir) => {
133
- await mkdir(dir, { recursive: true });
134
- return dir;
135
- });
136
- if (cancelled) {
137
- await rm(tempDir, { force: true, recursive: true });
138
- return;
139
- }
140
- tempDirRef.current = tempDir;
141
- // Resolve installed paths via the adapter
142
- const resolved = resolveInstalledPaths(installed, adapter, projectRoot);
143
- // Copy each installed file preserving sourcePath directory structure
144
- for (const file of resolved.files) {
145
- const sourceAbsolute = join(projectRoot, file.installedPath);
146
- const destPath = join(tempDir, file.sourcePath);
147
- const destDir = dirname(destPath);
148
- await mkdir(destDir, { recursive: true });
149
- await cp(sourceAbsolute, destPath);
150
- }
151
- // Generate manifest.json in the temp dir
152
- const manifest = {
153
- author: 'unknown',
154
- description: `Published from installed asset ${installed.name}`,
155
- entrypoint: installed.files[0]?.sourcePath ?? 'index.md',
156
- name: installed.name,
157
- ...(installed.org ? { org: installed.org } : {}),
158
- type: installed.type,
159
- version: installed.version,
160
- };
161
- await writeFile(join(tempDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
162
- if (!cancelled) {
163
- setResolvedPath(tempDir);
164
- setPhase('detecting');
165
- }
166
- };
167
- resolveInstalled().catch((err) => {
168
- if (!cancelled) {
169
- setErrorMessage(err instanceof Error ? err.message : String(err));
170
- setPhase('error');
171
- }
172
- });
173
- return () => {
174
- cancelled = true;
175
- };
176
- }, [phase, rawPath]);
177
- // ---------------------------------------------------------------------------
178
- // Phase: Detecting asset vs bundle
179
- // ---------------------------------------------------------------------------
180
- useEffect(() => {
181
- if (phase !== 'detecting')
182
- return;
183
- let cancelled = false;
184
- const detect = async () => {
185
- // First, try manifest-based detection (existing flow)
186
- try {
187
- const detected = await detectPublishType(resolvedPath);
188
- if (cancelled)
189
- return;
190
- setTarget(detected);
191
- setPhase('validating');
192
- return;
193
- }
194
- catch (manifestError) {
195
- // Only attempt raw detection if the error is specifically about missing manifest/bundle
196
- const manifestMsg = manifestError instanceof Error ? manifestError.message : String(manifestError);
197
- if (!manifestMsg.includes('No manifest.json or bundle.json found')) {
198
- // This is a different error (e.g., invalid JSON in manifest) — surface it directly
199
- if (!cancelled) {
200
- setErrorMessage(manifestMsg);
201
- setPhase('error');
202
- }
203
- return;
204
- }
205
- }
206
- // Manifest not found — attempt raw asset detection
207
- try {
208
- const { adapter } = await resolveTool(undefined);
209
- if (cancelled)
210
- return;
211
- const projectRoot = findProjectRoot();
212
- const detection = await detectAssetFromPath(resolvedPath, adapter, projectRoot);
213
- if (cancelled)
214
- return;
215
- setRawDetection(detection);
216
- setIsRawPublish(true);
217
- // Pre-fill metadata
218
- const author = (await getGitUserName()) ?? '';
219
- if (cancelled)
220
- return;
221
- setAuthorValue(author);
222
- const lockfile = await readLockfile(projectRoot);
223
- if (cancelled)
224
- return;
225
- setOrgValue(lockfile.org ?? '');
226
- const tools = await detectCompatibleTools(detection.assetType);
227
- if (cancelled)
228
- return;
229
- setToolsDetected(tools.map((t) => ({ tool: t })));
230
- // Non-interactive mode: both --description and --tags flags provided
231
- if (descriptionFlag && tagsFlag) {
232
- const tags = tagsFlag.split(',').map((t) => t.trim()).filter(Boolean);
233
- const stagingDir = await buildStagingArea(detection, {
234
- author,
235
- description: descriptionFlag,
236
- ...(lockfile.org ? { org: lockfile.org } : {}),
237
- tags,
238
- tools: tools.map((t) => ({ tool: t })),
239
- });
240
- if (cancelled)
241
- return;
242
- tempDirRef.current = stagingDir;
243
- const detected = await detectPublishType(stagingDir);
244
- if (cancelled)
245
- return;
246
- setTarget(detected);
247
- setPhase('validating');
248
- }
249
- else {
250
- // Interactive mode — gather metadata from the user
251
- setPhase('gathering-metadata');
252
- }
253
- }
254
- catch (rawError) {
255
- if (!cancelled) {
256
- setErrorMessage(rawError instanceof Error ? rawError.message : String(rawError));
257
- setPhase('error');
258
- }
259
- }
260
- };
261
- detect().catch((err) => {
262
- if (!cancelled) {
263
- setErrorMessage(err instanceof Error ? err.message : String(err));
264
- setPhase('error');
265
- }
266
- });
267
- return () => {
268
- cancelled = true;
269
- };
270
- }, [phase, resolvedPath, descriptionFlag, tagsFlag]);
271
- // ---------------------------------------------------------------------------
272
- // Phase: Gathering metadata — building staging area sub-phase
273
- // ---------------------------------------------------------------------------
274
- useEffect(() => {
275
- if (phase !== 'gathering-metadata' || metadataSubPhase !== 'building-staging')
276
- return;
277
- let cancelled = false;
278
- const build = async () => {
279
- // Parse tags from comma-separated string
280
- const tags = tagsValue
281
- .split(',')
282
- .map((t) => t.trim())
283
- .filter(Boolean);
284
- // Build staging area
285
- const stagingDir = await buildStagingArea(rawDetection, {
286
- author: authorValue,
287
- description: descriptionValue,
288
- org: orgValue || undefined,
289
- tags,
290
- tools: toolsDetected,
291
- });
292
- if (cancelled)
293
- return;
294
- // Store for cleanup
295
- tempDirRef.current = stagingDir;
296
- // Re-detect using the staging area (now has manifest.json)
297
- const detectedTarget = await detectPublishType(stagingDir);
298
- if (cancelled)
299
- return;
300
- setTarget(detectedTarget);
301
- setPhase('validating');
302
- };
303
- build().catch((err) => {
304
- if (!cancelled) {
305
- setErrorMessage(err instanceof Error ? err.message : String(err));
306
- setPhase('error');
307
- }
308
- });
309
- return () => {
310
- cancelled = true;
311
- };
312
- }, [phase, metadataSubPhase, authorValue, descriptionValue, orgValue, rawDetection, tagsValue, toolsDetected]);
313
- // ---------------------------------------------------------------------------
314
- // Phase: Validating manifest/bundle
315
- // ---------------------------------------------------------------------------
316
- useEffect(() => {
317
- if (phase !== 'validating')
318
- return;
319
- if (!target)
320
- return;
321
- let cancelled = false;
322
- const validate = async () => {
323
- const result = await validatePublishTarget(target);
324
- if (cancelled)
325
- return;
326
- setValidation(result);
327
- if (!result.valid) {
328
- setErrorMessage(`Validation failed:\n${result.errors.map((e) => ` - ${e}`).join('\n')}`);
329
- setPhase('error');
330
- return;
331
- }
332
- setPhase('checking-registry');
333
- };
334
- validate().catch((err) => {
335
- if (!cancelled) {
336
- setErrorMessage(err instanceof Error ? err.message : String(err));
337
- setPhase('error');
338
- }
339
- });
340
- return () => {
341
- cancelled = true;
342
- };
343
- }, [phase, target]);
344
- // ---------------------------------------------------------------------------
345
- // Phase: Checking registry for existing asset
346
- // ---------------------------------------------------------------------------
347
- useEffect(() => {
348
- if (phase !== 'checking-registry')
349
- return;
350
- if (!target)
351
- return;
352
- let cancelled = false;
353
- const check = async () => {
354
- const authToken = await getGitHubToken();
355
- if (cancelled)
356
- return;
357
- setToken(authToken);
358
- const reg = await fetchRegistry();
359
- if (cancelled)
360
- return;
361
- const versionResult = checkRegistryVersion(target, reg);
362
- if (cancelled)
363
- return;
364
- setVersionCheck(versionResult);
365
- if (versionResult.status === 'new-asset') {
366
- setIsUpdate(false);
367
- setResolvedVersion(target.data.version);
368
- setPhase('building-plan');
369
- }
370
- else if (versionResult.status === 'version-already-bumped') {
371
- setIsUpdate(true);
372
- setResolvedVersion(target.data.version);
373
- setPhase('confirm-update');
374
- }
375
- else {
376
- // needs-bump
377
- setIsUpdate(true);
378
- setPhase('bump-prompt');
379
- }
380
- };
381
- check().catch((err) => {
382
- if (!cancelled) {
383
- setErrorMessage(mapPublishError(err));
384
- setPhase('error');
385
- }
386
- });
387
- return () => {
388
- cancelled = true;
389
- };
390
- }, [phase, target]);
391
- // ---------------------------------------------------------------------------
392
- // Phase: Building publish plan
393
- // ---------------------------------------------------------------------------
394
- useEffect(() => {
395
- if (phase !== 'building-plan')
396
- return;
397
- if (!target)
398
- return;
399
- let cancelled = false;
400
- const build = async () => {
401
- const publishPlan = await buildPublishPlan(target, resolvedVersion, isUpdate, message);
402
- if (cancelled)
403
- return;
404
- setPlan(publishPlan);
405
- if (dryRun) {
406
- setPhase('dry-run-result');
407
- }
408
- else {
409
- setPhase('publishing');
410
- }
411
- };
412
- build().catch((err) => {
413
- if (!cancelled) {
414
- setErrorMessage(err instanceof Error ? err.message : String(err));
415
- setPhase('error');
416
- }
417
- });
418
- return () => {
419
- cancelled = true;
420
- };
421
- }, [phase, target, resolvedVersion, isUpdate, dryRun, message]);
422
- // ---------------------------------------------------------------------------
423
- // Phase: Publishing (executing git operations)
424
- // ---------------------------------------------------------------------------
425
- useEffect(() => {
426
- if (phase !== 'publishing')
427
- return;
428
- if (!plan || !token)
429
- return;
430
- let cancelled = false;
431
- const publish = async () => {
432
- const progressCallback = (msg) => {
433
- if (!cancelled) {
434
- setProgressMessage(msg);
435
- }
436
- };
437
- const result = isOrgAsset(plan)
438
- ? await executeDirectPublish(plan, token, progressCallback)
439
- : await executePublish(plan, token, progressCallback);
440
- if (cancelled)
441
- return;
442
- setPublishResult(result);
443
- // Clear registry cache so subsequent commands see the new version
444
- try {
445
- await clearCache();
446
- }
447
- catch {
448
- // Best-effort — don't fail publish over cache
449
- }
450
- // Update lockfile in-place when publishing from an installed asset
451
- if (fromInstalled) {
452
- try {
453
- const projectRoot = findProjectRoot();
454
- const { adapter: pubAdapter } = await resolveTool(undefined);
455
- await withLockfileLock(projectRoot, async () => {
456
- const lockfile = await readLockfile(projectRoot);
457
- const { name, org } = parseOrgFromName(rawPath);
458
- const installed = findInstalledAsset(lockfile, name, undefined, org);
459
- if (installed) {
460
- const resolved = resolveInstalledPaths(installed, pubAdapter, projectRoot);
461
- const updatedFiles = await Promise.all(resolved.files.map(async (file) => ({
462
- checksum: await hashFile(join(projectRoot, file.installedPath)),
463
- sourcePath: file.sourcePath,
464
- })));
465
- const updatedLockfile = addAssetToLockfile(lockfile, {
466
- ...installed,
467
- files: updatedFiles,
468
- version: resolvedVersion,
469
- });
470
- await writeLockfile(projectRoot, updatedLockfile);
471
- }
472
- });
473
- }
474
- catch {
475
- // Best-effort — don't fail publish over lockfile update
476
- }
477
- }
478
- // Raw publishes need to be added to the lockfile
479
- if (isRawPublish) {
480
- setPhase('installing');
481
- }
482
- else {
483
- setPhase('done');
484
- }
485
- };
486
- publish().catch((err) => {
487
- if (!cancelled) {
488
- setErrorMessage(mapPublishError(err));
489
- setPhase('error');
490
- }
491
- });
492
- return () => {
493
- cancelled = true;
494
- };
495
- }, [phase, plan, token, fromInstalled, rawPath, resolvedVersion, isRawPublish]);
496
- // ---------------------------------------------------------------------------
497
- // Phase: Installing (auto-install raw publish to lockfile)
498
- // ---------------------------------------------------------------------------
499
- useEffect(() => {
500
- if (phase !== 'installing')
501
- return;
502
- let cancelled = false;
503
- const install = async () => {
504
- try {
505
- const projectRoot = findProjectRoot();
506
- await withLockfileLock(projectRoot, async () => {
507
- // Read current lockfile
508
- const lockfile = await readLockfile(projectRoot);
509
- // Compute checksums from actual project files (not staging copies)
510
- const files = [];
511
- for (const file of rawDetection.files) {
512
- const checksum = await hashFile(file.absolutePath);
513
- files.push({
514
- checksum,
515
- sourcePath: file.relativePath,
516
- });
517
- }
518
- // Build InstalledAsset entry
519
- const installedAsset = {
520
- files,
521
- installedAt: new Date().toISOString(),
522
- installReason: 'direct',
523
- name: rawDetection.name,
524
- org: orgValue || undefined,
525
- type: rawDetection.assetType,
526
- version: resolvedVersion || target?.data.version || '1.0.0',
527
- };
528
- // Add to lockfile and write
529
- const updatedLockfile = addAssetToLockfile(lockfile, installedAsset);
530
- await writeLockfile(projectRoot, updatedLockfile);
531
- });
532
- if (!cancelled) {
533
- setPhase('done');
534
- }
535
- }
536
- catch (error) {
537
- // Publish already succeeded, so just warn and move to done
538
- if (!cancelled) {
539
- console.error('Warning: Failed to update lockfile:', error);
540
- setPhase('done');
541
- }
542
- }
543
- };
544
- install();
545
- return () => {
546
- cancelled = true;
547
- };
548
- }, [phase, rawDetection, orgValue, resolvedVersion, target]);
549
- // ---------------------------------------------------------------------------
550
- // Prompt handlers
551
- // ---------------------------------------------------------------------------
552
- const handleConfirmSubmit = useCallback((value) => {
553
- const trimmed = value.trim().toLowerCase();
554
- if (trimmed === 'y' || trimmed === 'yes') {
555
- setPhase('building-plan');
556
- }
557
- else if (trimmed === 'n' || trimmed === 'no') {
558
- setErrorMessage('Publish cancelled.');
559
- setPhase('error');
560
- }
561
- // Otherwise ignore (wait for valid input)
562
- }, []);
563
- const handleBumpSubmit = useCallback((value) => {
564
- if (!target || !versionCheck || versionCheck.status !== 'needs-bump')
565
- return;
566
- const trimmed = value.trim().toLowerCase();
567
- if (trimmed !== 'patch' && trimmed !== 'minor' && trimmed !== 'major') {
568
- return; // Ignore invalid input
569
- }
570
- const bumpType = trimmed;
571
- const newVersion = bumpVersion(versionCheck.latestVersion, bumpType);
572
- setResolvedVersion(newVersion);
573
- // Update the local manifest file
574
- const manifestFile = target.type === 'asset' ? 'manifest.json' : 'bundle.json';
575
- updateManifestVersion(target.sourceDir, manifestFile, newVersion)
576
- .then(() => {
577
- // Re-read the target so it has the updated version
578
- return detectPublishType(target.sourceDir);
579
- })
580
- .then((updatedTarget) => {
581
- setTarget(updatedTarget);
582
- setPhase('building-plan');
583
- })
584
- .catch((err) => {
585
- setErrorMessage(`Failed to update version: ${err instanceof Error ? err.message : String(err)}`);
586
- setPhase('error');
587
- });
588
- }, [target, versionCheck]);
589
- // ---------------------------------------------------------------------------
590
- // Raw publish prompt handlers
591
- // ---------------------------------------------------------------------------
592
- const handleDescriptionSubmit = useCallback((value) => {
593
- if (!value.trim())
594
- return; // Don't allow empty description
595
- setDescriptionValue(value.trim());
596
- setMetadataSubPhase('tags');
597
- }, []);
598
- const handleTagsSubmit = useCallback((value) => {
599
- setTagsValue(value);
600
- setMetadataSubPhase('building-staging');
601
- }, []);
53
+ const flags = {
54
+ descriptionFlag,
55
+ dryRun,
56
+ fromInstalled,
57
+ message,
58
+ rawPath,
59
+ tagsFlag,
60
+ };
61
+ const [state, dispatch] = usePublishState(flags);
602
62
  // ---------------------------------------------------------------------------
603
63
  // Exit on error
604
64
  // ---------------------------------------------------------------------------
605
65
  useEffect(() => {
606
- if (phase === 'error') {
607
- void cleanupTempDir();
66
+ if (state.phase === 'error') {
608
67
  const timer = setTimeout(() => {
609
68
  process.exitCode = 1;
610
69
  exit();
@@ -613,13 +72,12 @@ export default function Publish({ args: [rawPath], options: { description: descr
613
72
  clearTimeout(timer);
614
73
  };
615
74
  }
616
- }, [phase, exit, cleanupTempDir]);
75
+ }, [state.phase, exit]);
617
76
  // ---------------------------------------------------------------------------
618
77
  // Exit on done
619
78
  // ---------------------------------------------------------------------------
620
79
  useEffect(() => {
621
- if (phase === 'done') {
622
- void cleanupTempDir();
80
+ if (state.phase === 'done') {
623
81
  const timer = setTimeout(() => {
624
82
  exit();
625
83
  }, 100);
@@ -627,13 +85,12 @@ export default function Publish({ args: [rawPath], options: { description: descr
627
85
  clearTimeout(timer);
628
86
  };
629
87
  }
630
- }, [phase, exit, cleanupTempDir]);
88
+ }, [state.phase, exit]);
631
89
  // ---------------------------------------------------------------------------
632
90
  // Exit on dry-run-result
633
91
  // ---------------------------------------------------------------------------
634
92
  useEffect(() => {
635
- if (phase === 'dry-run-result') {
636
- void cleanupTempDir();
93
+ if (state.phase === 'dry-run-result') {
637
94
  const timer = setTimeout(() => {
638
95
  exit();
639
96
  }, 100);
@@ -641,64 +98,70 @@ export default function Publish({ args: [rawPath], options: { description: descr
641
98
  clearTimeout(timer);
642
99
  };
643
100
  }
644
- }, [phase, exit, cleanupTempDir]);
101
+ }, [state.phase, exit]);
645
102
  // ---------------------------------------------------------------------------
646
103
  // Render
647
104
  // ---------------------------------------------------------------------------
648
105
  // Error state
649
- if (phase === 'error') {
650
- return _jsxs(Text, { color: 'red', children: ["Error: ", errorMessage] });
106
+ if (state.phase === 'error') {
107
+ return _jsxs(Text, { color: 'red', children: ["Error: ", state.errorMessage] });
651
108
  }
652
109
  // Resolving installed asset
653
- if (phase === 'resolving-installed') {
110
+ if (state.phase === 'resolving-installed') {
654
111
  return _jsx(Spinner, { message: 'Resolving installed asset...' });
655
112
  }
656
113
  // Detecting
657
- if (phase === 'detecting') {
114
+ if (state.phase === 'detecting') {
658
115
  return _jsx(Spinner, { message: 'Detecting asset type...' });
659
116
  }
660
117
  // Gathering metadata (raw publish)
661
- if (phase === 'gathering-metadata' && rawDetection) {
662
- return (_jsxs(Box, { flexDirection: 'column', children: [_jsxs(Text, { children: ["Detected: ", rawDetection.assetType, " \"", _jsx(Text, { bold: true, children: rawDetection.name }), "\" (", rawDetection.files.length, " file", rawDetection.files.length !== 1 ? 's' : '', ")"] }), _jsxs(Text, { children: ["Author: ", authorValue, " (from git config)"] }), orgValue && _jsxs(Text, { children: ["Org: ", orgValue, " (from lockfile)"] }), _jsx(Text, { children: " " }), metadataSubPhase === 'description' && (_jsxs(Box, { children: [_jsx(Text, { children: "Description: " }), _jsx(TextInput, { focus: true, onChange: setDescriptionValue, onSubmit: handleDescriptionSubmit, value: descriptionValue })] })), metadataSubPhase === 'tags' && (_jsxs(Box, { flexDirection: 'column', children: [_jsxs(Text, { children: ["Description: ", descriptionValue] }), _jsxs(Box, { children: [_jsx(Text, { children: "Tags (comma-separated, optional): " }), _jsx(TextInput, { focus: true, onChange: setTagsValue, onSubmit: handleTagsSubmit, value: tagsValue })] })] })), metadataSubPhase === 'building-staging' && _jsx(Spinner, { message: 'Building staging area...' })] }));
118
+ if (state.phase === 'gathering-metadata' && state.rawDetection) {
119
+ return (_jsxs(Box, { flexDirection: 'column', children: [_jsxs(Text, { children: ["Detected: ", state.rawDetection.assetType, " \"", _jsx(Text, { bold: true, children: state.rawDetection.name }), "\" (", state.rawDetection.files.length, " file", state.rawDetection.files.length !== 1 ? 's' : '', ")"] }), _jsxs(Text, { children: ["Author: ", state.authorValue, " (from git config)"] }), state.orgValue && _jsxs(Text, { children: ["Org: ", state.orgValue, " (from lockfile)"] }), _jsx(Text, { children: " " }), state.metadataSubPhase === 'description' && (_jsxs(Box, { children: [_jsx(Text, { children: "Description: " }), _jsx(TextInput, { focus: true, onChange: (v) => dispatch({ type: 'SET_DESCRIPTION', value: v }), onSubmit: () => dispatch({ type: 'SUBMIT_DESCRIPTION' }), value: state.descriptionValue })] })), state.metadataSubPhase === 'tags' && (_jsxs(Box, { flexDirection: 'column', children: [_jsxs(Text, { children: ["Description: ", state.descriptionValue] }), _jsxs(Box, { children: [_jsx(Text, { children: "Tags (comma-separated, optional): " }), _jsx(TextInput, { focus: true, onChange: (v) => dispatch({ type: 'SET_TAGS', value: v }), onSubmit: () => dispatch({ type: 'SUBMIT_TAGS' }), value: state.tagsValue })] })] })), state.metadataSubPhase === 'building-staging' && _jsx(Spinner, { message: 'Building staging area...' })] }));
663
120
  }
664
121
  // Validating
665
- if (phase === 'validating') {
122
+ if (state.phase === 'validating') {
666
123
  return _jsx(Spinner, { message: 'Validating manifest...' });
667
124
  }
668
125
  // Checking registry
669
- if (phase === 'checking-registry') {
126
+ if (state.phase === 'checking-registry') {
670
127
  return _jsx(Spinner, { message: 'Checking registry...' });
671
128
  }
672
129
  // Confirm update prompt
673
- if (phase === 'confirm-update' && target && versionCheck && versionCheck.status === 'version-already-bumped') {
674
- return (_jsxs(Box, { flexDirection: 'column', children: [validation && (_jsxs(Box, { flexDirection: 'column', marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Validation:" }), validation.errors.length === 0 && (_jsxs(Text, { children: [' ', _jsx(CheckIcon, { status: 'pass' }), " All checks passed"] })), validation.warnings.map((w, i) => (_jsxs(Text, { children: [' ', _jsx(CheckIcon, { status: 'warn' }), " ", w] }, i)))] })), _jsxs(Box, { flexDirection: 'column', marginBottom: 1, children: [_jsxs(Text, { children: ["Asset ", _jsx(Text, { bold: true, children: target.data.name }), " already exists in the registry at v", versionCheck.latestVersion, "."] }), _jsxs(Text, { children: ["Publishing v", _jsx(Text, { bold: true, children: target.data.version }), " as an update."] })] }), _jsxs(Box, { children: [_jsx(Text, { color: 'cyan', children: '> ' }), _jsx(Text, { children: "Continue? (y/n): " }), _jsx(TextInput, { focus: true, onChange: setConfirmValue, onSubmit: handleConfirmSubmit, value: confirmValue })] })] }));
130
+ if (state.phase === 'confirm-update' &&
131
+ state.target &&
132
+ state.versionCheck &&
133
+ state.versionCheck.status === 'version-already-bumped') {
134
+ return (_jsxs(Box, { flexDirection: 'column', children: [state.validation && (_jsxs(Box, { flexDirection: 'column', marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Validation:" }), state.validation.errors.length === 0 && (_jsxs(Text, { children: [' ', _jsx(CheckIcon, { status: 'pass' }), " All checks passed"] })), state.validation.warnings.map((w, i) => (_jsxs(Text, { children: [' ', _jsx(CheckIcon, { status: 'warn' }), " ", w] }, i)))] })), _jsxs(Box, { flexDirection: 'column', marginBottom: 1, children: [_jsxs(Text, { children: ["Asset ", _jsx(Text, { bold: true, children: state.target.data.name }), " already exists in the registry at v", state.versionCheck.latestVersion, "."] }), _jsxs(Text, { children: ["Publishing v", _jsx(Text, { bold: true, children: state.target.data.version }), " as an update."] })] }), _jsxs(Box, { children: [_jsx(Text, { color: 'cyan', children: '> ' }), _jsx(Text, { children: "Continue? (y/n): " }), _jsx(TextInput, { focus: true, onChange: (v) => dispatch({ type: 'SET_CONFIRM_VALUE', value: v }), onSubmit: () => dispatch({ type: 'SUBMIT_CONFIRM' }), value: state.confirmValue })] })] }));
675
135
  }
676
136
  // Bump prompt
677
- if (phase === 'bump-prompt' && target && versionCheck && versionCheck.status === 'needs-bump') {
678
- return (_jsxs(Box, { flexDirection: 'column', children: [validation && (_jsxs(Box, { flexDirection: 'column', marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Validation:" }), validation.errors.length === 0 && (_jsxs(Text, { children: [' ', _jsx(CheckIcon, { status: 'pass' }), " All checks passed"] })), validation.warnings.map((w, i) => (_jsxs(Text, { children: [' ', _jsx(CheckIcon, { status: 'warn' }), " ", w] }, i)))] })), _jsxs(Box, { flexDirection: 'column', marginBottom: 1, children: [_jsxs(Text, { children: ["Asset ", _jsx(Text, { bold: true, children: target.data.name }), " exists in the registry at v", versionCheck.latestVersion, "."] }), _jsxs(Text, { children: ["Current manifest version (v", target.data.version, ") is not newer."] })] }), _jsxs(Box, { children: [_jsx(Text, { color: 'cyan', children: '> ' }), _jsx(Text, { children: "Bump type (patch/minor/major): " }), _jsx(TextInput, { focus: true, onChange: setBumpSelection, onSubmit: handleBumpSubmit, value: bumpSelection })] })] }));
137
+ if (state.phase === 'bump-prompt' &&
138
+ state.target &&
139
+ state.versionCheck &&
140
+ state.versionCheck.status === 'needs-bump') {
141
+ return (_jsxs(Box, { flexDirection: 'column', children: [state.validation && (_jsxs(Box, { flexDirection: 'column', marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Validation:" }), state.validation.errors.length === 0 && (_jsxs(Text, { children: [' ', _jsx(CheckIcon, { status: 'pass' }), " All checks passed"] })), state.validation.warnings.map((w, i) => (_jsxs(Text, { children: [' ', _jsx(CheckIcon, { status: 'warn' }), " ", w] }, i)))] })), _jsxs(Box, { flexDirection: 'column', marginBottom: 1, children: [_jsxs(Text, { children: ["Asset ", _jsx(Text, { bold: true, children: state.target.data.name }), " exists in the registry at v", state.versionCheck.latestVersion, "."] }), _jsxs(Text, { children: ["Current manifest version (v", state.target.data.version, ") is not newer."] })] }), _jsxs(Box, { children: [_jsx(Text, { color: 'cyan', children: '> ' }), _jsx(Text, { children: "Bump type (patch/minor/major): " }), _jsx(TextInput, { focus: true, onChange: (v) => dispatch({ type: 'SET_BUMP_SELECTION', value: v }), onSubmit: () => dispatch({ type: 'SUBMIT_BUMP' }), value: state.bumpSelection })] })] }));
679
142
  }
680
143
  // Building plan
681
- if (phase === 'building-plan') {
144
+ if (state.phase === 'building-plan') {
682
145
  return _jsx(Spinner, { message: 'Building publish plan...' });
683
146
  }
684
147
  // Dry run result
685
- if (phase === 'dry-run-result' && plan && target) {
686
- const statusLabel = plan.isUpdate
687
- ? `Update from v${versionCheck && versionCheck.status !== 'new-asset' ? versionCheck.latestVersion : '?'}`
148
+ if (state.phase === 'dry-run-result' && state.plan && state.target) {
149
+ const statusLabel = state.plan.isUpdate
150
+ ? `Update from v${state.versionCheck && state.versionCheck.status !== 'new-asset' ? state.versionCheck.latestVersion : '?'}`
688
151
  : 'New asset';
689
- return (_jsxs(Box, { flexDirection: 'column', children: [_jsx(DryRunBanner, { action: 'publish', count: plan.files.length, tool: 'registry' }), _jsx(Text, { children: " " }), validation && (_jsxs(Box, { flexDirection: 'column', marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Validation:" }), validation.errors.length === 0 && (_jsxs(Text, { children: [' ', _jsx(CheckIcon, { status: 'pass' }), " All checks passed"] })), validation.warnings.map((w, i) => (_jsxs(Text, { children: [' ', _jsx(CheckIcon, { status: 'warn' }), " ", w] }, i)))] })), _jsxs(Box, { flexDirection: 'column', children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Asset: " }), _jsx(Text, { bold: true, children: target.data.name })] }), target.type === 'asset' && target.assetType && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Type: " }), target.assetType] })), target.type === 'bundle' && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Type: " }), "bundle"] })), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Version: " }), plan.resolvedVersion] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Status: " }), statusLabel] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Checksum: " }), plan.checksum] })] }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Files to publish:" }), plan.files.map((file) => (_jsxs(Text, { children: [" ", file] }, file))), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Target: " }), plan.registryPath] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Branch: " }), plan.branchName] }), isOrgAsset(plan) ? (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Publish: " }), "Direct commit to ", DEFAULT_REGISTRY_BRANCH] })) : (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "PR Target:" }), " ", DEFAULT_REGISTRY_BRANCH] }))] }));
152
+ return (_jsxs(Box, { flexDirection: 'column', children: [_jsx(DryRunBanner, { action: 'publish', count: state.plan.files.length, tools: ['registry'] }), _jsx(Text, { children: " " }), state.validation && (_jsxs(Box, { flexDirection: 'column', marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Validation:" }), state.validation.errors.length === 0 && (_jsxs(Text, { children: [' ', _jsx(CheckIcon, { status: 'pass' }), " All checks passed"] })), state.validation.warnings.map((w, i) => (_jsxs(Text, { children: [' ', _jsx(CheckIcon, { status: 'warn' }), " ", w] }, i)))] })), _jsxs(Box, { flexDirection: 'column', children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Asset: " }), _jsx(Text, { bold: true, children: state.target.data.name })] }), state.target.type === 'asset' && state.target.assetType && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Type: " }), state.target.assetType] })), state.target.type === 'bundle' && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Type: " }), "bundle"] })), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Version: " }), state.plan.resolvedVersion] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Status: " }), statusLabel] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Checksum: " }), state.plan.checksum] })] }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Files to publish:" }), state.plan.files.map((file) => (_jsxs(Text, { children: [" ", file] }, file))), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Target: " }), state.plan.registryPath] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Branch: " }), state.plan.branchName] }), isOrgAsset(state.plan) ? (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Publish: " }), "Direct commit to ", DEFAULT_REGISTRY_BRANCH] })) : (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "PR Target:" }), " ", DEFAULT_REGISTRY_BRANCH] }))] }));
690
153
  }
691
154
  // Publishing
692
- if (phase === 'publishing') {
693
- return _jsx(Spinner, { message: progressMessage || 'Publishing...' });
155
+ if (state.phase === 'publishing') {
156
+ return _jsx(Spinner, { message: state.progressMessage || 'Publishing...' });
694
157
  }
695
158
  // Installing (auto-install raw publish to lockfile)
696
- if (phase === 'installing') {
159
+ if (state.phase === 'installing') {
697
160
  return _jsx(Spinner, { message: 'Adding to lockfile...' });
698
161
  }
699
162
  // Done
700
- if (phase === 'done' && publishResult && target) {
701
- return (_jsxs(Box, { flexDirection: 'column', children: [_jsxs(Text, { color: 'green', children: ["Published ", _jsx(Text, { bold: true, children: target.data.name }), " v", resolvedVersion] }), isRawPublish && (_jsxs(Text, { color: 'green', children: ["Installed ", rawDetection?.name, "@", resolvedVersion || target.data.version || '1.0.0', " to lockfile"] })), _jsx(Text, { children: " " }), publishResult.mode === 'pr' ? (_jsxs(Text, { children: ["PR: ", _jsx(Text, { color: 'cyan', children: publishResult.prUrl })] })) : (_jsxs(Text, { children: ["Committed directly to ", _jsx(Text, { color: 'cyan', children: publishResult.branchName })] })), _jsxs(Text, { children: ["Branch: ", _jsx(Text, { dimColor: true, children: publishResult.branchName })] })] }));
163
+ if (state.phase === 'done' && state.publishResult && state.target) {
164
+ return (_jsxs(Box, { flexDirection: 'column', children: [_jsxs(Text, { color: 'green', children: ["Published ", _jsx(Text, { bold: true, children: state.target.data.name }), " v", state.resolvedVersion] }), state.isRawPublish && (_jsxs(Text, { color: 'green', children: ["Installed ", state.rawDetection?.name, "@", state.resolvedVersion || state.target.data.version || '1.0.0', " to lockfile"] })), _jsx(Text, { children: " " }), state.publishResult.mode === 'pr' ? (_jsxs(Text, { children: ["PR: ", _jsx(Text, { color: 'cyan', children: state.publishResult.prUrl })] })) : (_jsxs(Text, { children: ["Committed directly to ", _jsx(Text, { color: 'cyan', children: state.publishResult.branchName })] })), _jsxs(Text, { children: ["Branch: ", _jsx(Text, { dimColor: true, children: state.publishResult.branchName })] })] }));
702
165
  }
703
166
  return null;
704
167
  }