@detergent-software/atk 8.1.2-dev.9 → 8.1.2
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/CHANGELOG.md +335 -0
- package/build/commands/audit.d.ts.map +1 -1
- package/build/commands/audit.js +14 -9
- package/build/commands/audit.js.map +1 -1
- package/build/commands/browse.js +21 -29
- package/build/commands/browse.js.map +1 -1
- package/build/commands/info.js +2 -2
- package/build/commands/info.js.map +1 -1
- package/build/commands/install.d.ts.map +1 -1
- package/build/commands/install.js +35 -75
- package/build/commands/install.js.map +1 -1
- package/build/commands/list.js +2 -2
- package/build/commands/list.js.map +1 -1
- package/build/commands/prune.d.ts.map +1 -1
- package/build/commands/prune.js +10 -8
- package/build/commands/prune.js.map +1 -1
- package/build/commands/sync.d.ts.map +1 -1
- package/build/commands/sync.js +5 -4
- package/build/commands/sync.js.map +1 -1
- package/build/commands/uninstall.d.ts +9 -1
- package/build/commands/uninstall.d.ts.map +1 -1
- package/build/commands/uninstall.js +40 -35
- package/build/commands/uninstall.js.map +1 -1
- package/build/commands/update.d.ts +9 -1
- package/build/commands/update.d.ts.map +1 -1
- package/build/commands/update.js +42 -12
- package/build/commands/update.js.map +1 -1
- package/build/commands/why.d.ts.map +1 -1
- package/build/commands/why.js +13 -5
- package/build/commands/why.js.map +1 -1
- package/build/components/AssetDetail.js +3 -1
- package/build/components/AssetDetail.js.map +1 -1
- package/build/components/AssetTable.d.ts.map +1 -1
- package/build/components/AssetTable.js +7 -3
- package/build/components/AssetTable.js.map +1 -1
- package/build/components/DependencyTree.d.ts +1 -1
- package/build/components/DependencyTree.d.ts.map +1 -1
- package/build/components/DependencyTree.js +4 -2
- package/build/components/DependencyTree.js.map +1 -1
- package/build/components/InstallSummary.d.ts +1 -0
- package/build/components/InstallSummary.d.ts.map +1 -1
- package/build/components/InstallSummary.js +3 -1
- package/build/components/InstallSummary.js.map +1 -1
- package/build/components/ListBrowseList.d.ts.map +1 -1
- package/build/components/ListBrowseList.js +4 -2
- package/build/components/ListBrowseList.js.map +1 -1
- package/build/components/OrphanListItem.d.ts +3 -1
- package/build/components/OrphanListItem.d.ts.map +1 -1
- package/build/components/OrphanListItem.js +3 -2
- package/build/components/OrphanListItem.js.map +1 -1
- package/build/components/SkippedAssetSection.d.ts +2 -0
- package/build/components/SkippedAssetSection.d.ts.map +1 -1
- package/build/components/SkippedAssetSection.js +1 -1
- package/build/components/SkippedAssetSection.js.map +1 -1
- package/build/components/SyncDriftedList.d.ts +3 -2
- package/build/components/SyncDriftedList.d.ts.map +1 -1
- package/build/components/SyncDriftedList.js +13 -3
- package/build/components/SyncDriftedList.js.map +1 -1
- package/build/hooks/useListActions.d.ts.map +1 -1
- package/build/hooks/useListActions.js +33 -16
- package/build/hooks/useListActions.js.map +1 -1
- package/build/hooks/usePruneState.d.ts +13 -15
- package/build/hooks/usePruneState.d.ts.map +1 -1
- package/build/hooks/usePruneState.js +10 -6
- package/build/hooks/usePruneState.js.map +1 -1
- package/build/hooks/useUninstallState.d.ts +16 -29
- package/build/hooks/useUninstallState.d.ts.map +1 -1
- package/build/hooks/useUninstallState.js +146 -139
- package/build/hooks/useUninstallState.js.map +1 -1
- package/build/lib/adapter.js +1 -1
- package/build/lib/adapter.js.map +1 -1
- package/build/lib/asset-tools.d.ts +5 -0
- package/build/lib/asset-tools.d.ts.map +1 -1
- package/build/lib/asset-tools.js +7 -0
- package/build/lib/asset-tools.js.map +1 -1
- package/build/lib/diagnostics.d.ts.map +1 -1
- package/build/lib/diagnostics.js +6 -5
- package/build/lib/diagnostics.js.map +1 -1
- package/build/lib/diff-command.d.ts.map +1 -1
- package/build/lib/diff-command.js +10 -7
- package/build/lib/diff-command.js.map +1 -1
- package/build/lib/enabled-list.d.ts +15 -0
- package/build/lib/enabled-list.d.ts.map +1 -0
- package/build/lib/enabled-list.js +83 -0
- package/build/lib/enabled-list.js.map +1 -0
- package/build/lib/file-snapshot.d.ts +17 -0
- package/build/lib/file-snapshot.d.ts.map +1 -0
- package/build/lib/file-snapshot.js +41 -0
- package/build/lib/file-snapshot.js.map +1 -0
- package/build/lib/installed-for-tool.d.ts +8 -0
- package/build/lib/installed-for-tool.d.ts.map +1 -0
- package/build/lib/installed-for-tool.js +18 -0
- package/build/lib/installed-for-tool.js.map +1 -0
- package/build/lib/installer.d.ts +109 -4
- package/build/lib/installer.d.ts.map +1 -1
- package/build/lib/installer.js +546 -162
- package/build/lib/installer.js.map +1 -1
- package/build/lib/lockfile.d.ts +30 -4
- package/build/lib/lockfile.d.ts.map +1 -1
- package/build/lib/lockfile.js +35 -15
- package/build/lib/lockfile.js.map +1 -1
- package/build/lib/multi-tool-install.d.ts +23 -0
- package/build/lib/multi-tool-install.d.ts.map +1 -0
- package/build/lib/multi-tool-install.js +78 -0
- package/build/lib/multi-tool-install.js.map +1 -0
- package/build/lib/org.d.ts +16 -0
- package/build/lib/org.d.ts.map +1 -1
- package/build/lib/org.js +18 -0
- package/build/lib/org.js.map +1 -1
- package/build/lib/owned-paths.d.ts +68 -0
- package/build/lib/owned-paths.d.ts.map +1 -0
- package/build/lib/owned-paths.js +162 -0
- package/build/lib/owned-paths.js.map +1 -0
- package/build/lib/registry.js +6 -2
- package/build/lib/registry.js.map +1 -1
- package/build/lib/resolver.d.ts.map +1 -1
- package/build/lib/resolver.js +35 -13
- package/build/lib/resolver.js.map +1 -1
- package/build/lib/sync.d.ts +8 -1
- package/build/lib/sync.d.ts.map +1 -1
- package/build/lib/sync.js +81 -28
- package/build/lib/sync.js.map +1 -1
- package/build/lib/uninstaller.d.ts +28 -4
- package/build/lib/uninstaller.d.ts.map +1 -1
- package/build/lib/uninstaller.js +95 -97
- package/build/lib/uninstaller.js.map +1 -1
- package/build/lib/updater.d.ts +59 -3
- package/build/lib/updater.d.ts.map +1 -1
- package/build/lib/updater.js +353 -80
- package/build/lib/updater.js.map +1 -1
- package/package.json +1 -1
package/build/lib/updater.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
1
2
|
import { compare, valid } from 'semver';
|
|
2
|
-
import {
|
|
3
|
+
import { getPlacement, resolveInstalledPaths } from './adapter.js';
|
|
4
|
+
import { isExcludedForTool, supportsType } from './asset-tools.js';
|
|
3
5
|
import { getCredential } from './auth.js';
|
|
6
|
+
import { getConfigEntriesForTool } from './config-entries.js';
|
|
4
7
|
import { ApiCodeError, asSentence, errorGuidance, errorMessage, isRunWideFailure, } from './errors.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
8
|
+
import { restoreFiles, snapshotFiles } from './file-snapshot.js';
|
|
9
|
+
import { executeInstallPlan, fetchPlanFiles, FileConflictError, installFailureMessage, InstallPlanError, } from './installer.js';
|
|
10
|
+
import { addAssetToLockfile, findDependencyAsset, findDependents, findInstalledAsset, getLockfilePath, readLockfile, writeLockfile, } from './lockfile.js';
|
|
11
|
+
import { formatOrgName, parseOrgFromName, scopeSwitchGuidance } from './org.js';
|
|
7
12
|
import { findAsset } from './registry.js';
|
|
8
13
|
import { resolveInstallPlan } from './resolver.js';
|
|
9
14
|
import { uninstallAsset } from './uninstaller.js';
|
|
@@ -19,8 +24,9 @@ export function checkOutdated(lockfile, registry, frozenNames) {
|
|
|
19
24
|
const outdated = [];
|
|
20
25
|
for (const installed of lockfile.assets ?? []) {
|
|
21
26
|
const resolved = findAsset(registry, installed.name, installed.type, undefined, installed.org);
|
|
22
|
-
if (!resolved) {
|
|
23
|
-
// Asset no longer in registry — skip (could be removed/deprecated)
|
|
27
|
+
if (!resolved || resolved.asset.org !== installed.org) {
|
|
28
|
+
// Asset no longer in registry — skip (could be removed/deprecated). For an org asset the
|
|
29
|
+
// lookup falls back to the global asset of the same name, which update does not switch to.
|
|
24
30
|
continue;
|
|
25
31
|
}
|
|
26
32
|
// resolved.version is the latest version (findAsset defaults to latest)
|
|
@@ -39,9 +45,38 @@ export function checkOutdated(lockfile, registry, frozenNames) {
|
|
|
39
45
|
}
|
|
40
46
|
return outdated;
|
|
41
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The installed asset `atk update <spec>` names. `@org/name` names that org's entry only. A
|
|
50
|
+
* bare name resolves the way `install` resolved it: the lockfile org's entry, else the global
|
|
51
|
+
* one. A name installed as more than one type needs `type` to pick one.
|
|
52
|
+
*/
|
|
53
|
+
export function findUpdateTarget(lockfile, spec, { type, version } = {}) {
|
|
54
|
+
const { name, org } = parseOrgFromName(spec);
|
|
55
|
+
const named = (lockfile.assets ?? []).filter((a) => a.name === name && (!type || a.type === type));
|
|
56
|
+
let matches = named.filter((a) => a.org === (org ?? lockfile.org));
|
|
57
|
+
if (matches.length === 0 && !org && lockfile.org) {
|
|
58
|
+
matches = named.filter((a) => a.org === undefined);
|
|
59
|
+
}
|
|
60
|
+
const versionSuffix = version ? `@${version}` : '';
|
|
61
|
+
if (matches.length > 1) {
|
|
62
|
+
const types = matches.map((a) => a.type).sort();
|
|
63
|
+
throw new UpdaterError(`"${spec}" is installed as more than one type (${types.join(', ')}). Name the one to update with --type.`, { command: `atk update ${spec}${versionSuffix} --type ${types[0]}` });
|
|
64
|
+
}
|
|
65
|
+
if (matches.length === 1)
|
|
66
|
+
return matches[0];
|
|
67
|
+
const elsewhere = [...new Set(named.map((a) => formatOrgName(a.org, a.name)))].sort();
|
|
68
|
+
if (elsewhere.length > 0) {
|
|
69
|
+
throw new UpdaterError(`Asset "${spec}" is not installed. Installed as: ${elsewhere.join(', ')}.`, elsewhere.length === 1
|
|
70
|
+
? { command: `atk update ${elsewhere[0]}${versionSuffix}${type ? ` --type ${type}` : ''}` }
|
|
71
|
+
: {});
|
|
72
|
+
}
|
|
73
|
+
throw new UpdaterError(`Asset "${spec}" is not installed.`);
|
|
74
|
+
}
|
|
42
75
|
/**
|
|
43
76
|
* Update a single asset to the target version (defaults to latest).
|
|
44
|
-
*
|
|
77
|
+
* Downloads the target version, then uninstalls the current version and installs the target.
|
|
78
|
+
* A failed uninstall or install puts back the files the update changed, from a snapshot taken after the
|
|
79
|
+
* download, so the rollback needs no second download.
|
|
45
80
|
*/
|
|
46
81
|
export async function updateAsset(assetName, installed, registry, options) {
|
|
47
82
|
const { adapter, onProgress, projectDir, targetVersion } = options;
|
|
@@ -53,6 +88,9 @@ export async function updateAsset(assetName, installed, registry, options) {
|
|
|
53
88
|
}
|
|
54
89
|
throw new UpdaterError(`Asset "${assetName}" not found in registry.`);
|
|
55
90
|
}
|
|
91
|
+
if (resolved.asset.org !== installed.org) {
|
|
92
|
+
throw scopeSwitchError(installed, resolved.asset);
|
|
93
|
+
}
|
|
56
94
|
const toVersion = resolved.version;
|
|
57
95
|
if (!isNewer(toVersion, installed.version)) {
|
|
58
96
|
if (targetVersion) {
|
|
@@ -63,71 +101,152 @@ export async function updateAsset(assetName, installed, registry, options) {
|
|
|
63
101
|
const fromVersion = installed.version;
|
|
64
102
|
// Capture old dependencies before uninstalling
|
|
65
103
|
const oldDeps = installed.dependencies ?? [];
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
onProgress?.(`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
//
|
|
79
|
-
|
|
104
|
+
// Download the target version before removing anything, so a refused download (an outdated
|
|
105
|
+
// CLI, an expired sign-in, the API down) leaves the installed version in place.
|
|
106
|
+
const plan = planFor(registry, installed, toVersion);
|
|
107
|
+
onProgress?.(`Downloading ${assetName}@${toVersion}...`);
|
|
108
|
+
let files;
|
|
109
|
+
try {
|
|
110
|
+
files = await fetchPlanFiles(plan, adapter);
|
|
111
|
+
}
|
|
112
|
+
catch (downloadError) {
|
|
113
|
+
throw new UpdaterError(`Failed to update "${assetName}" to v${toVersion}: ${failureReason(downloadError, installed)} ` +
|
|
114
|
+
`v${fromVersion} is still installed.`, errorGuidance(downloadError));
|
|
115
|
+
}
|
|
116
|
+
// Capture what the update can change, so a failed uninstall or install restores it without the network.
|
|
117
|
+
let snapshot;
|
|
118
|
+
// The version the lockfile records now. In a multi-tool update an earlier tool may already
|
|
119
|
+
// have moved it past `fromVersion`, which a restore then puts back.
|
|
120
|
+
let lockedVersion;
|
|
121
|
+
// The asset's files as this tool has them, which the uninstall removes.
|
|
122
|
+
let toolEntry;
|
|
123
|
+
try {
|
|
124
|
+
const lockfile = await readLockfile(projectDir);
|
|
125
|
+
lockedVersion = findInstalledAsset(lockfile, assetName, installed.type, installed.org)?.version;
|
|
126
|
+
const toolLockfile = options.toolLockfile ?? lockfile;
|
|
127
|
+
toolEntry = findInstalledAsset(toolLockfile, assetName, installed.type, installed.org) ?? installed;
|
|
128
|
+
snapshot = await snapshotFiles(updateTouchedPaths([lockfile, toolLockfile], plan, [installed, toolEntry], adapter, projectDir));
|
|
129
|
+
}
|
|
130
|
+
catch (snapshotError) {
|
|
131
|
+
throw new UpdaterError(`Failed to update "${assetName}" to v${toVersion}: ${failureReason(snapshotError, installed)} ` +
|
|
132
|
+
`v${fromVersion} is still installed.`, errorGuidance(snapshotError));
|
|
133
|
+
}
|
|
80
134
|
let results;
|
|
81
135
|
try {
|
|
82
|
-
|
|
136
|
+
// Uninstall current version without cascade — new version may still need the same deps.
|
|
137
|
+
// The lockfile entry is kept so the reinstall below can carry over the config entries
|
|
138
|
+
// other tools registered for this asset (the install replaces the entry either way).
|
|
139
|
+
onProgress?.(`Removing ${assetName}@${fromVersion}...`);
|
|
140
|
+
await uninstallAsset(assetName, {
|
|
141
|
+
adapter,
|
|
142
|
+
cascade: false,
|
|
143
|
+
keepLockfileEntry: true,
|
|
144
|
+
...(options.toolLockfile ? { managedFrom: options.toolLockfile } : {}),
|
|
145
|
+
onProgress,
|
|
146
|
+
org: installed.org,
|
|
147
|
+
plan: { dependents: [], orphanedDependencies: [], primary: toolEntry },
|
|
148
|
+
projectDir,
|
|
149
|
+
type: installed.type,
|
|
150
|
+
});
|
|
151
|
+
// Install target version (pass explicit version to resolver)
|
|
152
|
+
onProgress?.(`Installing ${assetName}@${toVersion}...`);
|
|
153
|
+
// Not forced: the paths this tool has are ATK-managed, so only an unmanaged file or another
|
|
154
|
+
// installed asset's file conflicts.
|
|
155
|
+
results = await executeInstallPlan(plan, {
|
|
83
156
|
adapter,
|
|
84
|
-
|
|
157
|
+
files,
|
|
158
|
+
...(options.toolLockfile ? { managedFrom: options.toolLockfile } : {}),
|
|
85
159
|
onProgress,
|
|
86
160
|
projectDir,
|
|
87
161
|
});
|
|
88
|
-
await restoreBundleOrigin(projectDir, installed);
|
|
89
162
|
}
|
|
90
163
|
catch (installError) {
|
|
91
|
-
//
|
|
92
|
-
|
|
164
|
+
// `atk update --force` overrides pins, not file conflicts, so the conflict gets its own advice.
|
|
165
|
+
const conflict = installError instanceof InstallPlanError && installError.cause instanceof FileConflictError
|
|
166
|
+
? { asset: installError.asset, owner: installError.cause.owner, where: installError.cause.where }
|
|
167
|
+
: undefined;
|
|
168
|
+
const owner = conflict?.owner && formatOrgName(conflict.owner.org, conflict.owner.name);
|
|
169
|
+
const installFailure = conflict
|
|
170
|
+
? `${conflict.where} ` +
|
|
171
|
+
(conflict.owner
|
|
172
|
+
? `belongs to the installed ${conflict.owner.type} "${owner}"`
|
|
173
|
+
: 'already exists and was not installed by ATK') +
|
|
174
|
+
(isSameAsset(conflict.asset, installed)
|
|
175
|
+
? '.'
|
|
176
|
+
: `, and the dependency "${formatOrgName(conflict.asset.org, conflict.asset.name)}" goes there.`)
|
|
177
|
+
: failureReason(installError, installed);
|
|
178
|
+
const conflictRemedy = owner ? `Uninstall "${owner}" first` : 'Move or rename it';
|
|
179
|
+
onProgress?.(`Update of ${assetName} to ${toVersion} failed, attempting to restore ${assetName}@${fromVersion}...`);
|
|
93
180
|
try {
|
|
94
|
-
await
|
|
95
|
-
adapter,
|
|
96
|
-
force: true,
|
|
97
|
-
onProgress,
|
|
98
|
-
projectDir,
|
|
99
|
-
});
|
|
100
|
-
await restoreBundleOrigin(projectDir, installed);
|
|
181
|
+
await restoreFiles(snapshot);
|
|
101
182
|
}
|
|
102
183
|
catch (rollbackError) {
|
|
103
|
-
|
|
104
|
-
|
|
184
|
+
const rollbackFailure = failureReason(rollbackError, installed);
|
|
185
|
+
throw new UpdaterError(`Failed to update "${assetName}" to v${toVersion}: ${installFailure} ` +
|
|
186
|
+
`Rollback to v${fromVersion} also failed` +
|
|
187
|
+
(rollbackFailure === installFailure ? ' for the same reason. ' : `: ${rollbackFailure} `) +
|
|
105
188
|
`Run "atk sync" to restore the asset.`, { command: 'atk sync', ...errorGuidance(installError) });
|
|
106
189
|
}
|
|
107
|
-
|
|
108
|
-
|
|
190
|
+
if (lockedVersion !== undefined && lockedVersion !== fromVersion) {
|
|
191
|
+
// The lockfile has one entry for every tool, so this tool's files now trail it.
|
|
192
|
+
const tool = adapter.tool;
|
|
193
|
+
const command = `atk sync --tool ${tool}`;
|
|
194
|
+
throw new UpdaterError(`Failed to update "${assetName}" to v${toVersion} for ${tool}: ${installFailure} ` +
|
|
195
|
+
`Its ${tool} files were restored to v${fromVersion}, but the lockfile records v${lockedVersion}, ` +
|
|
196
|
+
`which the other tools have. Run "${command}" to install v${lockedVersion} for ${tool}.`, conflict
|
|
197
|
+
? { command, hint: `${conflictRemedy}, then run the command.` }
|
|
198
|
+
: { ...errorGuidance(installError), command });
|
|
199
|
+
}
|
|
200
|
+
throw new UpdaterError(`Failed to update "${assetName}" to v${toVersion}: ${installFailure} ` +
|
|
201
|
+
`The previous version (v${fromVersion}) has been restored.`, conflict ? { hint: `${conflictRemedy}, then run the update again.` } : errorGuidance(installError));
|
|
109
202
|
}
|
|
110
|
-
|
|
203
|
+
// After the restore path: the new version is installed, and restoring the old lockfile would
|
|
204
|
+
// orphan the files of the dependencies it added.
|
|
205
|
+
await restoreBundleOrigin(projectDir, installed);
|
|
206
|
+
// Match org and version too: the plan can hold a same-named dependency from another org.
|
|
207
|
+
const directResult = results.find((r) => r.name === assetName && r.type === installed.type && r.org === installed.org && r.version === toVersion);
|
|
111
208
|
if (!directResult) {
|
|
112
209
|
throw new UpdaterError(`Install did not produce a result for "${assetName}".`);
|
|
113
210
|
}
|
|
114
|
-
// Clean up dropped dependencies: deps in old version but not in new version
|
|
211
|
+
// Clean up dropped dependencies: deps in old version but not in new version. One asset's
|
|
212
|
+
// dependency refs carry no org, so type and name identify each.
|
|
115
213
|
const newDeps = resolved.versionData.dependencies ?? [];
|
|
116
214
|
const newDepKeys = new Set(newDeps.map((d) => `${d.type}:${d.name}`));
|
|
117
215
|
const droppedDeps = oldDeps.filter((d) => !newDepKeys.has(`${d.type}:${d.name}`));
|
|
216
|
+
// The new version is installed, so a cleanup failure is a warning, not a failed update.
|
|
217
|
+
const warnings = [];
|
|
118
218
|
if (droppedDeps.length > 0) {
|
|
119
|
-
|
|
219
|
+
// Read after the install: before it, the old entry still lists the dropped deps as its own.
|
|
220
|
+
const updatedLockfile = await readLockfile(projectDir);
|
|
120
221
|
for (const dep of droppedDeps) {
|
|
121
|
-
const depAsset =
|
|
222
|
+
const depAsset = findDependencyAsset(updatedLockfile, dep, installed.org);
|
|
122
223
|
if (!depAsset)
|
|
123
224
|
continue;
|
|
124
|
-
// Only remove if it was a dependency and no other asset depends on it
|
|
225
|
+
// Only remove if it was a dependency and no other asset depends on it for this tool
|
|
125
226
|
if ((depAsset.installReason ?? 'direct') !== 'dependency')
|
|
126
227
|
continue;
|
|
127
|
-
const dependents = findDependents(updatedLockfile,
|
|
228
|
+
const dependents = findDependents(updatedLockfile, depAsset.name, depAsset.type, depAsset.org).filter((d) => !isExcludedForTool(d, adapter.tool));
|
|
128
229
|
if (dependents.length === 0) {
|
|
129
230
|
onProgress?.(`Removing dropped dependency ${dep.name}...`);
|
|
130
|
-
|
|
231
|
+
try {
|
|
232
|
+
await uninstallAsset(dep.name, {
|
|
233
|
+
adapter,
|
|
234
|
+
cascade: true,
|
|
235
|
+
onProgress,
|
|
236
|
+
org: depAsset.org,
|
|
237
|
+
projectDir,
|
|
238
|
+
toolScope: { configuredAdapters: options.configuredAdapters ?? [adapter] },
|
|
239
|
+
type: depAsset.type,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
const message = `Could not remove dropped dependency "${dep.name}": ${asSentence(errorMessage(error))}`;
|
|
244
|
+
onProgress?.(`Warning: ${message}`);
|
|
245
|
+
// Other tools may still need the dependency, so the command removes it for this tool only.
|
|
246
|
+
const toolFlag = (options.configuredAdapters?.length ?? 1) > 1 ? ` --tool ${adapter.tool}` : '';
|
|
247
|
+
const spec = `${depAsset.org ? `@${depAsset.org}/` : ''}${dep.name}`;
|
|
248
|
+
warnings.push({ command: `atk uninstall ${spec}${toolFlag}`, message });
|
|
249
|
+
}
|
|
131
250
|
}
|
|
132
251
|
}
|
|
133
252
|
}
|
|
@@ -135,8 +254,10 @@ export async function updateAsset(assetName, installed, registry, options) {
|
|
|
135
254
|
fromVersion,
|
|
136
255
|
installResult: directResult,
|
|
137
256
|
name: assetName,
|
|
257
|
+
...(installed.org ? { org: installed.org } : {}),
|
|
138
258
|
toVersion,
|
|
139
259
|
type: installed.type,
|
|
260
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
140
261
|
};
|
|
141
262
|
}
|
|
142
263
|
/**
|
|
@@ -148,23 +269,24 @@ export async function updateAssets(assetNames, lockfile, registry, options) {
|
|
|
148
269
|
// When a specific target version is requested for a single asset, bypass outdated check
|
|
149
270
|
// and delegate directly to updateAsset which handles version comparison.
|
|
150
271
|
if (options.targetVersion && assetNames && assetNames.length === 1) {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
272
|
+
const installed = findUpdateTarget(lockfile, assetNames[0], {
|
|
273
|
+
type: options.type,
|
|
274
|
+
version: options.targetVersion,
|
|
275
|
+
});
|
|
276
|
+
const { name } = installed;
|
|
277
|
+
const spec = assetSpec(installed, options.type);
|
|
156
278
|
// Check frozen state for explicitly targeted asset
|
|
157
279
|
const compositeKey = `${installed.type}:${installed.org ?? ''}:${name}`;
|
|
158
280
|
if (options.frozenAssets?.has(compositeKey)) {
|
|
159
|
-
throw new UpdaterError(`Asset "${name}" is frozen. Use "atk thaw ${
|
|
160
|
-
command: `atk thaw ${
|
|
281
|
+
throw new UpdaterError(`Asset "${name}" is frozen. Use "atk thaw ${spec}" to unfreeze it before updating.`, {
|
|
282
|
+
command: `atk thaw ${spec}`,
|
|
161
283
|
hint: 'Thaw the asset before updating, or remove it from your lockfile.',
|
|
162
284
|
});
|
|
163
285
|
}
|
|
164
286
|
// Check pin state for explicitly targeted asset
|
|
165
287
|
if (installed.pinnedVersion && !options.force) {
|
|
166
288
|
throw new UpdaterError(`Asset "${name}" is pinned to version "${installed.pinnedVersion}". Use --force to override the pin.`, {
|
|
167
|
-
command: `atk unpin ${
|
|
289
|
+
command: `atk unpin ${spec}`,
|
|
168
290
|
hint: 'Unpin the asset, or pass --force to override the pin.',
|
|
169
291
|
});
|
|
170
292
|
}
|
|
@@ -174,10 +296,21 @@ export async function updateAssets(assetNames, lockfile, registry, options) {
|
|
|
174
296
|
options.onProgress?.(`Skipping ${name}: uninstalled for ${options.adapter.tool}`);
|
|
175
297
|
return { excludedSkipped: [toExcludedSkipped(installed)], failures: [], results: [] };
|
|
176
298
|
}
|
|
177
|
-
const
|
|
299
|
+
const failedSkip = failedSkipFor(installed, lockfile, await recordedLockfile(options), options);
|
|
300
|
+
if (failedSkip) {
|
|
301
|
+
return { failedSkipped: [failedSkip], failures: [], results: [] };
|
|
302
|
+
}
|
|
303
|
+
if (!supportsType(options.adapter, installed.type)) {
|
|
304
|
+
reportUnsupported(installed, options);
|
|
305
|
+
return { failures: [], results: [] };
|
|
306
|
+
}
|
|
307
|
+
const dependencySkip = await failedDependencySkipFor(installed, registry, options);
|
|
308
|
+
if (dependencySkip) {
|
|
309
|
+
return { failedSkipped: [dependencySkip], failures: [], results: [] };
|
|
310
|
+
}
|
|
178
311
|
const result = await updateAsset(name, installed, registry, {
|
|
179
312
|
...options,
|
|
180
|
-
|
|
313
|
+
toolLockfile: options.toolLockfile ?? lockfile,
|
|
181
314
|
});
|
|
182
315
|
return { failures: [], results: [result] };
|
|
183
316
|
}
|
|
@@ -186,22 +319,24 @@ export async function updateAssets(assetNames, lockfile, registry, options) {
|
|
|
186
319
|
const frozenSkipped = [];
|
|
187
320
|
const skipped = [];
|
|
188
321
|
if (assetNames && assetNames.length > 0) {
|
|
189
|
-
|
|
322
|
+
const targets = assetNames.map((spec) => findUpdateTarget(lockfile, spec, { type: options.type }));
|
|
323
|
+
toUpdate = outdated.filter((o) => targets.some((t) => isSameAsset(t, o)));
|
|
190
324
|
// Check for names that aren't outdated
|
|
191
|
-
for (const
|
|
192
|
-
if (!outdated.
|
|
193
|
-
const
|
|
194
|
-
if (
|
|
195
|
-
throw
|
|
325
|
+
for (const target of targets) {
|
|
326
|
+
if (!outdated.some((o) => isSameAsset(target, o))) {
|
|
327
|
+
const resolved = findAsset(registry, target.name, target.type, undefined, target.org);
|
|
328
|
+
if (resolved && resolved.asset.org !== target.org) {
|
|
329
|
+
throw scopeSwitchError(target, resolved.asset);
|
|
196
330
|
}
|
|
197
|
-
throw new UpdaterError(`Asset "${name}" is already at the latest version (v${
|
|
331
|
+
throw new UpdaterError(`Asset "${formatOrgName(target.org, target.name)}" is already at the latest version (v${target.version}).`);
|
|
198
332
|
}
|
|
199
333
|
}
|
|
200
334
|
// For explicitly targeted frozen assets, throw
|
|
201
335
|
for (const asset of toUpdate) {
|
|
202
336
|
if (asset.frozen) {
|
|
203
|
-
|
|
204
|
-
|
|
337
|
+
const spec = assetSpec(asset, options.type);
|
|
338
|
+
throw new UpdaterError(`Asset "${asset.name}" is frozen. Use "atk thaw ${spec}" to unfreeze it before updating.`, {
|
|
339
|
+
command: `atk thaw ${spec}`,
|
|
205
340
|
hint: 'Thaw the asset before updating, or remove it from your lockfile.',
|
|
206
341
|
});
|
|
207
342
|
}
|
|
@@ -210,7 +345,7 @@ export async function updateAssets(assetNames, lockfile, registry, options) {
|
|
|
210
345
|
for (const asset of toUpdate) {
|
|
211
346
|
if (asset.pinned && !options.force) {
|
|
212
347
|
throw new UpdaterError(`Asset "${asset.name}" is pinned. Use --force to override the pin.`, {
|
|
213
|
-
command: `atk unpin ${asset.
|
|
348
|
+
command: `atk unpin ${assetSpec(asset, options.type)}`,
|
|
214
349
|
hint: 'Unpin the asset, or pass --force to override the pin.',
|
|
215
350
|
});
|
|
216
351
|
}
|
|
@@ -235,45 +370,59 @@ export async function updateAssets(assetNames, lockfile, registry, options) {
|
|
|
235
370
|
}
|
|
236
371
|
// Assets uninstalled for this tool are never updated for it (that would reinstall them).
|
|
237
372
|
const excludedSkipped = [];
|
|
373
|
+
const failedSkipped = [];
|
|
374
|
+
const recorded = await recordedLockfile(options);
|
|
238
375
|
toUpdate = toUpdate.filter((asset) => {
|
|
239
|
-
const installed = (lockfile
|
|
376
|
+
const installed = lockfileEntryFor(lockfile, asset);
|
|
240
377
|
if (installed && isExcludedForTool(installed, options.adapter.tool)) {
|
|
241
378
|
options.onProgress?.(`Skipping ${asset.name}: uninstalled for ${options.adapter.tool}`);
|
|
242
379
|
excludedSkipped.push(toExcludedSkipped(installed));
|
|
243
380
|
return false;
|
|
244
381
|
}
|
|
382
|
+
const failedSkip = failedSkipFor(asset, lockfile, recorded, options);
|
|
383
|
+
if (failedSkip) {
|
|
384
|
+
failedSkipped.push(failedSkip);
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
// The tool has no placement for the type (a rule under Codex CLI): nothing to update.
|
|
388
|
+
if (!supportsType(options.adapter, asset.type)) {
|
|
389
|
+
reportUnsupported(asset, options);
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
245
392
|
return true;
|
|
246
393
|
});
|
|
247
394
|
if (toUpdate.length === 0) {
|
|
248
395
|
return {
|
|
249
396
|
...(excludedSkipped.length > 0 ? { excludedSkipped } : {}),
|
|
397
|
+
...(failedSkipped.length > 0 ? { failedSkipped } : {}),
|
|
250
398
|
failures: [],
|
|
251
399
|
frozenSkipped: frozenSkipped.length > 0 ? frozenSkipped : undefined,
|
|
252
400
|
results: [],
|
|
253
401
|
skipped: skipped.length > 0 ? skipped : undefined,
|
|
254
402
|
};
|
|
255
403
|
}
|
|
256
|
-
// Resolve the credential
|
|
257
|
-
//
|
|
258
|
-
//
|
|
404
|
+
// Resolve the credential once, up front. A missing or expired sign-in carries no API code for
|
|
405
|
+
// isRunWideFailure, and with a fresh registry cache it would otherwise fail each asset's
|
|
406
|
+
// download in turn.
|
|
259
407
|
await getCredential();
|
|
260
408
|
const results = [];
|
|
261
409
|
const failures = [];
|
|
262
410
|
let notAttempted;
|
|
411
|
+
// What this tool has: the lockfile the run started from, plus what this tool's updates install.
|
|
412
|
+
let toolLockfile = options.toolLockfile ?? lockfile;
|
|
263
413
|
for (const [index, asset] of toUpdate.entries()) {
|
|
264
|
-
const installed = (lockfile
|
|
414
|
+
const installed = lockfileEntryFor(lockfile, asset);
|
|
265
415
|
if (!installed) {
|
|
266
416
|
throw new UpdaterError(`Asset "${asset.name}" is installed but not found in lockfile.`);
|
|
267
417
|
}
|
|
418
|
+
let result;
|
|
268
419
|
try {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
});
|
|
276
|
-
results.push(result);
|
|
420
|
+
const dependencySkip = await failedDependencySkipFor(installed, registry, options);
|
|
421
|
+
if (dependencySkip) {
|
|
422
|
+
failedSkipped.push(dependencySkip);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
result = await updateAsset(asset.name, installed, registry, { ...options, toolLockfile });
|
|
277
426
|
}
|
|
278
427
|
catch (error) {
|
|
279
428
|
// Most errors from updateAsset already name the asset; only frame the ones that don't.
|
|
@@ -283,16 +432,27 @@ export async function updateAssets(assetNames, lockfile, registry, options) {
|
|
|
283
432
|
: `Failed to update "${asset.name}": ${asSentence(cause)}`;
|
|
284
433
|
options.onProgress?.(message);
|
|
285
434
|
const guidance = errorGuidance(error);
|
|
286
|
-
failures.push({
|
|
287
|
-
|
|
435
|
+
failures.push({
|
|
436
|
+
error: message,
|
|
437
|
+
name: asset.name,
|
|
438
|
+
...(asset.org ? { org: asset.org } : {}),
|
|
439
|
+
type: asset.type,
|
|
440
|
+
...guidance,
|
|
441
|
+
});
|
|
442
|
+
// The same error would fail every remaining asset.
|
|
288
443
|
if (isRunWideFailure(guidance)) {
|
|
289
444
|
notAttempted = toUpdate.slice(index + 1).map((a) => a.name);
|
|
290
445
|
break;
|
|
291
446
|
}
|
|
447
|
+
continue;
|
|
292
448
|
}
|
|
449
|
+
results.push(result);
|
|
450
|
+
// The update succeeded either way; a failed read leaves the next update to report it.
|
|
451
|
+
toolLockfile = await withInstalledEntries(toolLockfile, planFor(registry, installed, result.toVersion), options).catch(() => toolLockfile);
|
|
293
452
|
}
|
|
294
453
|
return {
|
|
295
454
|
...(excludedSkipped.length > 0 ? { excludedSkipped } : {}),
|
|
455
|
+
...(failedSkipped.length > 0 ? { failedSkipped } : {}),
|
|
296
456
|
failures,
|
|
297
457
|
frozenSkipped: frozenSkipped.length > 0 ? frozenSkipped : undefined,
|
|
298
458
|
...(notAttempted ? { notAttempted } : {}),
|
|
@@ -300,6 +460,60 @@ export async function updateAssets(assetNames, lockfile, registry, options) {
|
|
|
300
460
|
skipped: skipped.length > 0 ? skipped : undefined,
|
|
301
461
|
};
|
|
302
462
|
}
|
|
463
|
+
/** How a command names an asset: `@org/name`, with `--type` when the run was given one. */
|
|
464
|
+
function assetSpec(asset, type) {
|
|
465
|
+
return `${formatOrgName(asset.org, asset.name)}${type ? ` --type ${asset.type}` : ''}`;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* The skip record for an asset whose update would move a dependency whose own update failed for
|
|
469
|
+
* an earlier tool in the run: that tool kept the version the lockfile records, so moving it here
|
|
470
|
+
* would leave the tools on different versions. A dependency the lockfile already records at the
|
|
471
|
+
* plan's version, or one this tool has no placement for, is not moved, so it does not count.
|
|
472
|
+
*/
|
|
473
|
+
async function failedDependencySkipFor(installed, registry, options) {
|
|
474
|
+
if (!options.failedFor?.size)
|
|
475
|
+
return undefined;
|
|
476
|
+
// An asset that does not resolve gets its error from the update itself.
|
|
477
|
+
const resolved = findAsset(registry, installed.name, installed.type, options.targetVersion, installed.org);
|
|
478
|
+
if (!resolved || resolved.asset.org !== installed.org)
|
|
479
|
+
return undefined;
|
|
480
|
+
const lockfile = await readLockfile(options.projectDir);
|
|
481
|
+
for (const { asset, version } of planFor(registry, installed, resolved.version).entries) {
|
|
482
|
+
if (isSameAsset(asset, installed) || !supportsType(options.adapter, asset.type))
|
|
483
|
+
continue;
|
|
484
|
+
const failedFor = options.failedFor.get(`${asset.type}:${asset.org ?? ''}:${asset.name}`);
|
|
485
|
+
if (!failedFor || findInstalledAsset(lockfile, asset.name, asset.type, asset.org)?.version === version)
|
|
486
|
+
continue;
|
|
487
|
+
options.onProgress?.(`Skipping ${installed.name} for ${options.adapter.tool}: its dependency ${asset.name} failed to update for ${failedFor}`);
|
|
488
|
+
return {
|
|
489
|
+
dependency: { name: asset.name, ...(asset.org ? { org: asset.org } : {}), type: asset.type },
|
|
490
|
+
failedFor,
|
|
491
|
+
name: installed.name,
|
|
492
|
+
...(installed.org ? { org: installed.org } : {}),
|
|
493
|
+
type: installed.type,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* The skip record for an asset whose update failed for an earlier tool in the run, if it did. An
|
|
500
|
+
* asset that another asset's plan moved for that tool after all (the lockfile no longer records
|
|
501
|
+
* the run's starting version) is not skipped: the later tools move it too.
|
|
502
|
+
*/
|
|
503
|
+
function failedSkipFor(asset, snapshot, recorded, options) {
|
|
504
|
+
const failedFor = options.failedFor?.get(`${asset.type}:${asset.org ?? ''}:${asset.name}`);
|
|
505
|
+
if (!failedFor)
|
|
506
|
+
return undefined;
|
|
507
|
+
const versionIn = (lockfile) => lockfile && findInstalledAsset(lockfile, asset.name, asset.type, asset.org)?.version;
|
|
508
|
+
if (versionIn(recorded) !== versionIn(snapshot))
|
|
509
|
+
return undefined;
|
|
510
|
+
options.onProgress?.(`Skipping ${asset.name} for ${options.adapter.tool}: its update failed for ${failedFor}`);
|
|
511
|
+
return { failedFor, name: asset.name, ...(asset.org ? { org: asset.org } : {}), type: asset.type };
|
|
512
|
+
}
|
|
513
|
+
/** Why an install for an update failed, as a sentence; the update message already names the asset. */
|
|
514
|
+
function failureReason(error, installed) {
|
|
515
|
+
return asSentence(installFailureMessage(error, installed));
|
|
516
|
+
}
|
|
303
517
|
/**
|
|
304
518
|
* Check if the registry version is newer than the installed version.
|
|
305
519
|
* Falls back to string comparison if semver parsing fails.
|
|
@@ -313,19 +527,35 @@ function isNewer(registryVersion, installedVersion) {
|
|
|
313
527
|
// silently ignored.
|
|
314
528
|
return registryVersion !== installedVersion;
|
|
315
529
|
}
|
|
530
|
+
function isSameAsset(a, b) {
|
|
531
|
+
return a.name === b.name && a.type === b.type && a.org === b.org;
|
|
532
|
+
}
|
|
533
|
+
/** The lockfile entry of an outdated asset: same name, type and org. */
|
|
534
|
+
function lockfileEntryFor(lockfile, asset) {
|
|
535
|
+
return (lockfile.assets ?? []).find((a) => isSameAsset(a, asset));
|
|
536
|
+
}
|
|
316
537
|
/**
|
|
317
538
|
* The install plan for one version of an installed asset. The resolver treats the asset as
|
|
318
539
|
* a fresh top-level request, so keep the install reason its lockfile entry had: updating a
|
|
319
|
-
* dependency must not turn it into a direct install.
|
|
540
|
+
* dependency must not turn it into a direct install. Only the requested asset's entry is
|
|
541
|
+
* `direct`, so a same-named entry, such as a dependency of another type or org, keeps its
|
|
542
|
+
* own reason.
|
|
320
543
|
*/
|
|
321
544
|
function planFor(registry, installed, version) {
|
|
322
545
|
const plan = resolveInstallPlan(registry, installed.name, installed.type, version, installed.org);
|
|
323
546
|
const reason = installed.installReason ?? 'direct';
|
|
324
547
|
return {
|
|
325
548
|
...plan,
|
|
326
|
-
entries: plan.entries.map((e) => e.
|
|
549
|
+
entries: plan.entries.map((e) => (e.reason === 'direct' ? { ...e, reason } : e)),
|
|
327
550
|
};
|
|
328
551
|
}
|
|
552
|
+
/** The lockfile on disk, read only when an earlier tool in the run had a failed update. */
|
|
553
|
+
async function recordedLockfile(options) {
|
|
554
|
+
return options.failedFor?.size ? readLockfile(options.projectDir) : undefined;
|
|
555
|
+
}
|
|
556
|
+
function reportUnsupported(asset, options) {
|
|
557
|
+
options.onProgress?.(`Skipping ${asset.name}: ${options.adapter.tool} has no placement for ${asset.type} assets`);
|
|
558
|
+
}
|
|
329
559
|
/**
|
|
330
560
|
* Put a bundle member's bundle origin back on its updated lockfile entry. Passing it to
|
|
331
561
|
* executeInstallPlan would also stamp the asset's dependencies, so it is set here instead.
|
|
@@ -339,7 +569,50 @@ async function restoreBundleOrigin(projectDir, installed) {
|
|
|
339
569
|
return;
|
|
340
570
|
await writeLockfile(projectDir, addAssetToLockfile(lockfile, { ...entry, bundleOrigin: installed.bundleOrigin }));
|
|
341
571
|
}
|
|
572
|
+
function scopeSwitchError(installed, fallback) {
|
|
573
|
+
const { message, ...guidance } = scopeSwitchGuidance(installed, fallback, 'update');
|
|
574
|
+
return new UpdaterError(message, guidance);
|
|
575
|
+
}
|
|
342
576
|
function toExcludedSkipped(installed) {
|
|
343
577
|
return { name: installed.name, ...(installed.org ? { org: installed.org } : {}), type: installed.type };
|
|
344
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* The absolute paths that updating `installed` with `plan` can change for the adapter's tool:
|
|
581
|
+
* the lockfile, the placed files and config files of the asset and of every installed asset the
|
|
582
|
+
* plan replaces (its dependencies), and the config files the plan's new assets register in. Each
|
|
583
|
+
* entry of `installed` and each lockfile of `lockfiles` is a view of what is installed; all count.
|
|
584
|
+
*/
|
|
585
|
+
function updateTouchedPaths(lockfiles, plan, installed, adapter, projectDir) {
|
|
586
|
+
const relativePaths = [];
|
|
587
|
+
const replaced = lockfiles.flatMap((lockfile) => plan.entries.flatMap((e) => findInstalledAsset(lockfile, e.asset.name, e.asset.type, e.asset.org) ?? []));
|
|
588
|
+
for (const asset of [...installed, ...replaced]) {
|
|
589
|
+
const resolved = resolveInstalledPaths(asset, adapter, projectDir);
|
|
590
|
+
relativePaths.push(...resolved.files.map((f) => f.installedPath));
|
|
591
|
+
relativePaths.push(...[...getConfigEntriesForTool(asset, adapter), ...resolved.configEntries].map((e) => e.file));
|
|
592
|
+
}
|
|
593
|
+
for (const { asset } of plan.entries) {
|
|
594
|
+
const placement = getPlacement(adapter, asset.type);
|
|
595
|
+
if (!placement)
|
|
596
|
+
continue;
|
|
597
|
+
if (placement.configFile)
|
|
598
|
+
relativePaths.push(placement.configFile);
|
|
599
|
+
if (placement.enabledListFile)
|
|
600
|
+
relativePaths.push(placement.enabledListFile);
|
|
601
|
+
// A memory template is spliced into a shared file at its placement path.
|
|
602
|
+
if (asset.type === 'memory-template')
|
|
603
|
+
relativePaths.push(placement.path.replace(/\{name\}/g, asset.name));
|
|
604
|
+
}
|
|
605
|
+
return [getLockfilePath(projectDir), ...relativePaths.map((p) => join(projectDir, p))];
|
|
606
|
+
}
|
|
607
|
+
/** `toolLockfile` with the on-disk lockfile entries of the assets an update installed with `plan`. */
|
|
608
|
+
async function withInstalledEntries(toolLockfile, plan, { projectDir }) {
|
|
609
|
+
const lockfile = await readLockfile(projectDir);
|
|
610
|
+
let updated = toolLockfile;
|
|
611
|
+
for (const { asset } of plan.entries) {
|
|
612
|
+
const entry = findInstalledAsset(lockfile, asset.name, asset.type, asset.org);
|
|
613
|
+
if (entry)
|
|
614
|
+
updated = addAssetToLockfile(updated, entry);
|
|
615
|
+
}
|
|
616
|
+
return updated;
|
|
617
|
+
}
|
|
345
618
|
//# sourceMappingURL=updater.js.map
|