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