@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.
- package/build/commands/audit.d.ts.map +1 -1
- package/build/commands/audit.js +15 -10
- package/build/commands/audit.js.map +1 -1
- package/build/commands/browse.d.ts.map +1 -1
- package/build/commands/browse.js +39 -114
- package/build/commands/browse.js.map +1 -1
- package/build/commands/diff.d.ts.map +1 -1
- package/build/commands/diff.js +4 -2
- package/build/commands/diff.js.map +1 -1
- package/build/commands/info.d.ts.map +1 -1
- package/build/commands/info.js +15 -2
- package/build/commands/info.js.map +1 -1
- package/build/commands/install.d.ts.map +1 -1
- package/build/commands/install.js +47 -32
- package/build/commands/install.js.map +1 -1
- package/build/commands/list.d.ts.map +1 -1
- package/build/commands/list.js +18 -8
- package/build/commands/list.js.map +1 -1
- package/build/commands/outdated.d.ts.map +1 -1
- package/build/commands/outdated.js +7 -6
- package/build/commands/outdated.js.map +1 -1
- package/build/commands/prune.d.ts.map +1 -1
- package/build/commands/prune.js +36 -47
- package/build/commands/prune.js.map +1 -1
- package/build/commands/publish.d.ts.map +1 -1
- package/build/commands/publish.js +46 -583
- package/build/commands/publish.js.map +1 -1
- package/build/commands/setup.d.ts +1 -1
- package/build/commands/setup.d.ts.map +1 -1
- package/build/commands/setup.js +49 -242
- package/build/commands/setup.js.map +1 -1
- package/build/commands/sync.d.ts.map +1 -1
- package/build/commands/sync.js +111 -120
- package/build/commands/sync.js.map +1 -1
- package/build/commands/uninstall.d.ts.map +1 -1
- package/build/commands/uninstall.js +103 -218
- package/build/commands/uninstall.js.map +1 -1
- package/build/commands/update.d.ts.map +1 -1
- package/build/commands/update.js +33 -23
- package/build/commands/update.js.map +1 -1
- package/build/components/AssetTable.d.ts +2 -1
- package/build/components/AssetTable.d.ts.map +1 -1
- package/build/components/AssetTable.js +5 -2
- package/build/components/AssetTable.js.map +1 -1
- package/build/components/DryRunBanner.d.ts +3 -3
- package/build/components/DryRunBanner.d.ts.map +1 -1
- package/build/components/DryRunBanner.js +2 -2
- package/build/components/DryRunBanner.js.map +1 -1
- package/build/components/InstallSummary.d.ts +3 -3
- package/build/components/InstallSummary.d.ts.map +1 -1
- package/build/components/InstallSummary.js +2 -2
- package/build/components/InstallSummary.js.map +1 -1
- package/build/hooks/useBrowseState.d.ts +23 -0
- package/build/hooks/useBrowseState.d.ts.map +1 -1
- package/build/hooks/useBrowseState.js +77 -1
- package/build/hooks/useBrowseState.js.map +1 -1
- package/build/hooks/useConfirmation.d.ts +18 -0
- package/build/hooks/useConfirmation.d.ts.map +1 -0
- package/build/hooks/useConfirmation.js +56 -0
- package/build/hooks/useConfirmation.js.map +1 -0
- package/build/hooks/useInitState.d.ts.map +1 -1
- package/build/hooks/useInitState.js +3 -2
- package/build/hooks/useInitState.js.map +1 -1
- package/build/hooks/usePublishState.d.ts +115 -0
- package/build/hooks/usePublishState.d.ts.map +1 -0
- package/build/hooks/usePublishState.js +670 -0
- package/build/hooks/usePublishState.js.map +1 -0
- package/build/hooks/useSetupState.d.ts +59 -0
- package/build/hooks/useSetupState.d.ts.map +1 -0
- package/build/hooks/useSetupState.js +297 -0
- package/build/hooks/useSetupState.js.map +1 -0
- package/build/hooks/useUninstallState.d.ts +102 -0
- package/build/hooks/useUninstallState.d.ts.map +1 -0
- package/build/hooks/useUninstallState.js +335 -0
- package/build/hooks/useUninstallState.js.map +1 -0
- package/build/lib/config.d.ts +4 -1
- package/build/lib/config.d.ts.map +1 -1
- package/build/lib/config.js +9 -9
- package/build/lib/config.js.map +1 -1
- package/build/lib/diagnostics.js +1 -1
- package/build/lib/diagnostics.js.map +1 -1
- package/build/lib/installer.js +1 -0
- package/build/lib/installer.js.map +1 -1
- package/build/lib/lockfile.d.ts +13 -13
- package/build/lib/lockfile.d.ts.map +1 -1
- package/build/lib/lockfile.js +39 -40
- package/build/lib/lockfile.js.map +1 -1
- package/build/lib/schemas/config.d.ts +7 -3
- package/build/lib/schemas/config.d.ts.map +1 -1
- package/build/lib/schemas/config.js +12 -2
- package/build/lib/schemas/config.js.map +1 -1
- package/build/lib/schemas/lockfile.d.ts +2 -0
- package/build/lib/schemas/lockfile.d.ts.map +1 -1
- package/build/lib/schemas/lockfile.js +1 -0
- package/build/lib/schemas/lockfile.js.map +1 -1
- package/build/lib/tool-resolver.d.ts +17 -6
- package/build/lib/tool-resolver.d.ts.map +1 -1
- package/build/lib/tool-resolver.js +31 -15
- package/build/lib/tool-resolver.js.map +1 -1
- package/build/lib/uninstaller.d.ts.map +1 -1
- package/build/lib/uninstaller.js +5 -2
- package/build/lib/uninstaller.js.map +1 -1
- package/build/lib/updater.d.ts.map +1 -1
- package/build/lib/updater.js +5 -1
- package/build/lib/updater.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,670 @@
|
|
|
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.split(',').map((t) => t.trim()).filter(Boolean);
|
|
344
|
+
const stagingDir = await buildStagingArea(detection, {
|
|
345
|
+
author,
|
|
346
|
+
description: state.flags.descriptionFlag,
|
|
347
|
+
...(lockfile.org ? { org: lockfile.org } : {}),
|
|
348
|
+
tags,
|
|
349
|
+
tools: tools.map((t) => ({ tool: t })),
|
|
350
|
+
});
|
|
351
|
+
if (cancelled)
|
|
352
|
+
return;
|
|
353
|
+
tempDirRef.current = stagingDir;
|
|
354
|
+
const detected = await detectPublishType(stagingDir);
|
|
355
|
+
if (cancelled)
|
|
356
|
+
return;
|
|
357
|
+
dispatch({ rawDetection: detection, target: detected, type: 'RAW_DETECT_DONE_NON_INTERACTIVE' });
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
// Interactive mode — gather metadata from the user
|
|
361
|
+
dispatch({
|
|
362
|
+
authorValue: author,
|
|
363
|
+
orgValue: orgVal,
|
|
364
|
+
rawDetection: detection,
|
|
365
|
+
toolsDetected: tools.map((t) => ({ tool: t })),
|
|
366
|
+
type: 'RAW_DETECT_DONE',
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
detect().catch((err) => {
|
|
371
|
+
if (!cancelled) {
|
|
372
|
+
dispatch({ message: err instanceof Error ? err.message : String(err), type: 'ERROR' });
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
return () => {
|
|
376
|
+
cancelled = true;
|
|
377
|
+
};
|
|
378
|
+
// Phase-gated effect: only runs when entering 'detecting'
|
|
379
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
380
|
+
}, [state.phase]);
|
|
381
|
+
// --- Phase: gathering-metadata / building-staging sub-phase ---
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
if (state.phase !== 'gathering-metadata' || state.metadataSubPhase !== 'building-staging')
|
|
384
|
+
return;
|
|
385
|
+
let cancelled = false;
|
|
386
|
+
const build = async () => {
|
|
387
|
+
// Parse tags from comma-separated string
|
|
388
|
+
const tags = state.tagsValue
|
|
389
|
+
.split(',')
|
|
390
|
+
.map((t) => t.trim())
|
|
391
|
+
.filter(Boolean);
|
|
392
|
+
// Build staging area
|
|
393
|
+
const stagingDir = await buildStagingArea(state.rawDetection, {
|
|
394
|
+
author: state.authorValue,
|
|
395
|
+
description: state.descriptionValue,
|
|
396
|
+
org: state.orgValue || undefined,
|
|
397
|
+
tags,
|
|
398
|
+
tools: state.toolsDetected,
|
|
399
|
+
});
|
|
400
|
+
if (cancelled)
|
|
401
|
+
return;
|
|
402
|
+
// Store for cleanup
|
|
403
|
+
tempDirRef.current = stagingDir;
|
|
404
|
+
// Re-detect using the staging area (now has manifest.json)
|
|
405
|
+
const detectedTarget = await detectPublishType(stagingDir);
|
|
406
|
+
if (cancelled)
|
|
407
|
+
return;
|
|
408
|
+
dispatch({ target: detectedTarget, type: 'STAGING_DONE' });
|
|
409
|
+
};
|
|
410
|
+
build().catch((err) => {
|
|
411
|
+
if (!cancelled) {
|
|
412
|
+
dispatch({ message: err instanceof Error ? err.message : String(err), type: 'ERROR' });
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
return () => {
|
|
416
|
+
cancelled = true;
|
|
417
|
+
};
|
|
418
|
+
// Phase-gated effect: only runs when entering building-staging sub-phase
|
|
419
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
420
|
+
}, [state.phase, state.metadataSubPhase]);
|
|
421
|
+
// --- Phase: validating ---
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
if (state.phase !== 'validating')
|
|
424
|
+
return;
|
|
425
|
+
if (!state.target)
|
|
426
|
+
return;
|
|
427
|
+
let cancelled = false;
|
|
428
|
+
const validate = async () => {
|
|
429
|
+
const result = await validatePublishTarget(state.target);
|
|
430
|
+
if (cancelled)
|
|
431
|
+
return;
|
|
432
|
+
dispatch({ type: 'VALIDATE_DONE', validation: result });
|
|
433
|
+
};
|
|
434
|
+
validate().catch((err) => {
|
|
435
|
+
if (!cancelled) {
|
|
436
|
+
dispatch({ message: err instanceof Error ? err.message : String(err), type: 'ERROR' });
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
return () => {
|
|
440
|
+
cancelled = true;
|
|
441
|
+
};
|
|
442
|
+
// Phase-gated effect: only runs when entering 'validating'
|
|
443
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
444
|
+
}, [state.phase]);
|
|
445
|
+
// --- Phase: checking-registry ---
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
if (state.phase !== 'checking-registry')
|
|
448
|
+
return;
|
|
449
|
+
if (!state.target)
|
|
450
|
+
return;
|
|
451
|
+
let cancelled = false;
|
|
452
|
+
const check = async () => {
|
|
453
|
+
const authToken = await getGitHubToken();
|
|
454
|
+
if (cancelled)
|
|
455
|
+
return;
|
|
456
|
+
const reg = await fetchRegistry({ force: state.flags.refresh });
|
|
457
|
+
if (cancelled)
|
|
458
|
+
return;
|
|
459
|
+
const versionResult = checkRegistryVersion(state.target, reg);
|
|
460
|
+
if (cancelled)
|
|
461
|
+
return;
|
|
462
|
+
let isUpdate = false;
|
|
463
|
+
let resolvedVersion = '';
|
|
464
|
+
if (versionResult.status === 'new-asset') {
|
|
465
|
+
isUpdate = false;
|
|
466
|
+
resolvedVersion = state.target.data.version;
|
|
467
|
+
}
|
|
468
|
+
else if (versionResult.status === 'version-already-bumped') {
|
|
469
|
+
isUpdate = true;
|
|
470
|
+
resolvedVersion = state.target.data.version;
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
// needs-bump
|
|
474
|
+
isUpdate = true;
|
|
475
|
+
}
|
|
476
|
+
if (cancelled)
|
|
477
|
+
return;
|
|
478
|
+
dispatch({
|
|
479
|
+
isUpdate,
|
|
480
|
+
resolvedVersion,
|
|
481
|
+
token: authToken,
|
|
482
|
+
type: 'REGISTRY_CHECK_DONE',
|
|
483
|
+
versionCheck: versionResult,
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
check().catch((err) => {
|
|
487
|
+
if (!cancelled) {
|
|
488
|
+
dispatch({ message: mapPublishError(err), type: 'ERROR' });
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
return () => {
|
|
492
|
+
cancelled = true;
|
|
493
|
+
};
|
|
494
|
+
// Phase-gated effect: only runs when entering 'checking-registry'
|
|
495
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
496
|
+
}, [state.phase]);
|
|
497
|
+
// --- Phase: building-plan ---
|
|
498
|
+
// This effect also handles the async manifest update when coming from bump-prompt
|
|
499
|
+
useEffect(() => {
|
|
500
|
+
if (state.phase !== 'building-plan')
|
|
501
|
+
return;
|
|
502
|
+
if (!state.target)
|
|
503
|
+
return;
|
|
504
|
+
let cancelled = false;
|
|
505
|
+
const build = async () => {
|
|
506
|
+
let currentTarget = state.target;
|
|
507
|
+
const currentVersion = state.resolvedVersion;
|
|
508
|
+
// If we came from a bump submission, update the manifest file first
|
|
509
|
+
if (state.versionCheck?.status === 'needs-bump' && currentVersion) {
|
|
510
|
+
const manifestFile = currentTarget.type === 'asset' ? 'manifest.json' : 'bundle.json';
|
|
511
|
+
await updateManifestVersion(currentTarget.sourceDir, manifestFile, currentVersion);
|
|
512
|
+
if (cancelled)
|
|
513
|
+
return;
|
|
514
|
+
// Re-read the target so it has the updated version
|
|
515
|
+
currentTarget = await detectPublishType(currentTarget.sourceDir);
|
|
516
|
+
if (cancelled)
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const publishPlan = await buildPublishPlan(currentTarget, currentVersion, state.isUpdate, state.flags.message);
|
|
520
|
+
if (cancelled)
|
|
521
|
+
return;
|
|
522
|
+
dispatch({ plan: publishPlan, type: 'PLAN_DONE' });
|
|
523
|
+
};
|
|
524
|
+
build().catch((err) => {
|
|
525
|
+
if (!cancelled) {
|
|
526
|
+
dispatch({ message: err instanceof Error ? err.message : String(err), type: 'ERROR' });
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
return () => {
|
|
530
|
+
cancelled = true;
|
|
531
|
+
};
|
|
532
|
+
// Phase-gated effect: only runs when entering 'building-plan'
|
|
533
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
534
|
+
}, [state.phase]);
|
|
535
|
+
// --- Phase: publishing ---
|
|
536
|
+
useEffect(() => {
|
|
537
|
+
if (state.phase !== 'publishing')
|
|
538
|
+
return;
|
|
539
|
+
if (!state.plan || !state.token)
|
|
540
|
+
return;
|
|
541
|
+
let cancelled = false;
|
|
542
|
+
const publish = async () => {
|
|
543
|
+
const progressCallback = (msg) => {
|
|
544
|
+
if (!cancelled) {
|
|
545
|
+
dispatch({ message: msg, type: 'PUBLISH_PROGRESS' });
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
const result = isOrgAsset(state.plan)
|
|
549
|
+
? await executeDirectPublish(state.plan, state.token, progressCallback)
|
|
550
|
+
: await executePublish(state.plan, state.token, progressCallback);
|
|
551
|
+
if (cancelled)
|
|
552
|
+
return;
|
|
553
|
+
// Clear registry cache so subsequent commands see the new version
|
|
554
|
+
try {
|
|
555
|
+
await clearCache();
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
// Best-effort — don't fail publish over cache
|
|
559
|
+
}
|
|
560
|
+
// Update lockfile in-place when publishing from an installed asset
|
|
561
|
+
if (state.flags.fromInstalled) {
|
|
562
|
+
try {
|
|
563
|
+
const projectRoot = findProjectRoot(state.flags.projectRoot);
|
|
564
|
+
const pubTools = await resolveTools(state.flags.tool);
|
|
565
|
+
const pubAdapter = pubTools[0].adapter;
|
|
566
|
+
await withLockfileLock(projectRoot, async () => {
|
|
567
|
+
const lockfile = await readLockfile(projectRoot);
|
|
568
|
+
const { name, org } = parseOrgFromName(state.flags.rawPath);
|
|
569
|
+
const installed = findInstalledAsset(lockfile, name, undefined, org);
|
|
570
|
+
if (installed) {
|
|
571
|
+
const resolved = resolveInstalledPaths(installed, pubAdapter, projectRoot);
|
|
572
|
+
const updatedFiles = await Promise.all(resolved.files.map(async (file) => ({
|
|
573
|
+
checksum: await hashFile(join(projectRoot, file.installedPath)),
|
|
574
|
+
sourcePath: file.sourcePath,
|
|
575
|
+
})));
|
|
576
|
+
const updatedLockfile = addAssetToLockfile(lockfile, {
|
|
577
|
+
...installed,
|
|
578
|
+
files: updatedFiles,
|
|
579
|
+
version: state.resolvedVersion,
|
|
580
|
+
});
|
|
581
|
+
await writeLockfile(projectRoot, updatedLockfile);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
// Best-effort — don't fail publish over lockfile update
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (cancelled)
|
|
590
|
+
return;
|
|
591
|
+
dispatch({ result, type: 'PUBLISH_DONE' });
|
|
592
|
+
};
|
|
593
|
+
publish().catch((err) => {
|
|
594
|
+
if (!cancelled) {
|
|
595
|
+
dispatch({ message: mapPublishError(err), type: 'ERROR' });
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
return () => {
|
|
599
|
+
cancelled = true;
|
|
600
|
+
};
|
|
601
|
+
// Phase-gated effect: only runs when entering 'publishing'
|
|
602
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
603
|
+
}, [state.phase]);
|
|
604
|
+
// --- Phase: installing (auto-install raw publish to lockfile) ---
|
|
605
|
+
useEffect(() => {
|
|
606
|
+
if (state.phase !== 'installing')
|
|
607
|
+
return;
|
|
608
|
+
let cancelled = false;
|
|
609
|
+
const install = async () => {
|
|
610
|
+
try {
|
|
611
|
+
const projectRoot = findProjectRoot(state.flags.projectRoot);
|
|
612
|
+
await withLockfileLock(projectRoot, async () => {
|
|
613
|
+
// Read current lockfile
|
|
614
|
+
const lockfile = await readLockfile(projectRoot);
|
|
615
|
+
// Compute checksums from actual project files (not staging copies)
|
|
616
|
+
const files = [];
|
|
617
|
+
for (const file of state.rawDetection.files) {
|
|
618
|
+
const checksum = await hashFile(file.absolutePath);
|
|
619
|
+
files.push({
|
|
620
|
+
checksum,
|
|
621
|
+
sourcePath: file.relativePath,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
// Build InstalledAsset entry
|
|
625
|
+
const installedAsset = {
|
|
626
|
+
files,
|
|
627
|
+
installedAt: new Date().toISOString(),
|
|
628
|
+
installReason: 'direct',
|
|
629
|
+
name: state.rawDetection.name,
|
|
630
|
+
org: state.orgValue || undefined,
|
|
631
|
+
type: state.rawDetection.assetType,
|
|
632
|
+
version: state.resolvedVersion || state.target?.data.version || '1.0.0',
|
|
633
|
+
};
|
|
634
|
+
// Add to lockfile and write
|
|
635
|
+
const updatedLockfile = addAssetToLockfile(lockfile, installedAsset);
|
|
636
|
+
await writeLockfile(projectRoot, updatedLockfile);
|
|
637
|
+
});
|
|
638
|
+
if (!cancelled) {
|
|
639
|
+
dispatch({ type: 'INSTALL_DONE' });
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
// Publish already succeeded, so just warn and move to done
|
|
644
|
+
if (!cancelled) {
|
|
645
|
+
dispatch({ type: 'INSTALL_DONE' });
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
install();
|
|
650
|
+
return () => {
|
|
651
|
+
cancelled = true;
|
|
652
|
+
};
|
|
653
|
+
// Phase-gated effect: only runs when entering 'installing'
|
|
654
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
655
|
+
}, [state.phase]);
|
|
656
|
+
// --- Cleanup temp directory on error or done ---
|
|
657
|
+
useEffect(() => {
|
|
658
|
+
if (state.phase !== 'error' && state.phase !== 'done' && state.phase !== 'dry-run-result')
|
|
659
|
+
return;
|
|
660
|
+
if (tempDirRef.current) {
|
|
661
|
+
const dir = tempDirRef.current;
|
|
662
|
+
tempDirRef.current = null;
|
|
663
|
+
rm(dir, { force: true, recursive: true }).catch(() => {
|
|
664
|
+
// Best-effort cleanup — ignore errors
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}, [state.phase]);
|
|
668
|
+
return [state, dispatch];
|
|
669
|
+
}
|
|
670
|
+
//# sourceMappingURL=usePublishState.js.map
|