@detergent-software/atk 8.1.2-dev.3 → 8.1.2-dev.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/build/commands/browse.js +5 -9
  3. package/build/commands/browse.js.map +1 -1
  4. package/build/commands/install.js +2 -2
  5. package/build/commands/install.js.map +1 -1
  6. package/build/commands/list.js +2 -2
  7. package/build/commands/list.js.map +1 -1
  8. package/build/commands/login.js +2 -2
  9. package/build/commands/login.js.map +1 -1
  10. package/build/commands/register.js +3 -3
  11. package/build/commands/register.js.map +1 -1
  12. package/build/commands/sync.d.ts.map +1 -1
  13. package/build/commands/sync.js +16 -11
  14. package/build/commands/sync.js.map +1 -1
  15. package/build/commands/uninstall.d.ts +9 -1
  16. package/build/commands/uninstall.d.ts.map +1 -1
  17. package/build/commands/uninstall.js +23 -26
  18. package/build/commands/uninstall.js.map +1 -1
  19. package/build/commands/update.d.ts +9 -1
  20. package/build/commands/update.d.ts.map +1 -1
  21. package/build/commands/update.js +54 -14
  22. package/build/commands/update.js.map +1 -1
  23. package/build/components/InstallConfirmBar.d.ts.map +1 -1
  24. package/build/components/InstallConfirmBar.js +2 -1
  25. package/build/components/InstallConfirmBar.js.map +1 -1
  26. package/build/components/ListActionBar.d.ts.map +1 -1
  27. package/build/components/ListActionBar.js +3 -2
  28. package/build/components/ListActionBar.js.map +1 -1
  29. package/build/components/RunStoppedHint.d.ts +13 -0
  30. package/build/components/RunStoppedHint.d.ts.map +1 -0
  31. package/build/components/RunStoppedHint.js +12 -0
  32. package/build/components/RunStoppedHint.js.map +1 -0
  33. package/build/components/SkippedAssetSection.d.ts +2 -0
  34. package/build/components/SkippedAssetSection.d.ts.map +1 -1
  35. package/build/components/SkippedAssetSection.js +1 -1
  36. package/build/components/SkippedAssetSection.js.map +1 -1
  37. package/build/components/SyncDriftedList.d.ts +3 -2
  38. package/build/components/SyncDriftedList.d.ts.map +1 -1
  39. package/build/components/SyncDriftedList.js +11 -3
  40. package/build/components/SyncDriftedList.js.map +1 -1
  41. package/build/components/index.d.ts +1 -0
  42. package/build/components/index.d.ts.map +1 -1
  43. package/build/components/index.js +1 -0
  44. package/build/components/index.js.map +1 -1
  45. package/build/hooks/useCommand.js +2 -2
  46. package/build/hooks/useCommand.js.map +1 -1
  47. package/build/hooks/useInitState.js +5 -5
  48. package/build/hooks/useInitState.js.map +1 -1
  49. package/build/hooks/useListActions.d.ts.map +1 -1
  50. package/build/hooks/useListActions.js +34 -18
  51. package/build/hooks/useListActions.js.map +1 -1
  52. package/build/hooks/usePruneState.js +7 -7
  53. package/build/hooks/usePruneState.js.map +1 -1
  54. package/build/hooks/usePublishState.js +3 -3
  55. package/build/hooks/usePublishState.js.map +1 -1
  56. package/build/hooks/useRegistryChangeState.js +2 -2
  57. package/build/hooks/useRegistryChangeState.js.map +1 -1
  58. package/build/hooks/useSetupState.js +3 -3
  59. package/build/hooks/useSetupState.js.map +1 -1
  60. package/build/hooks/useUninstallState.d.ts +16 -29
  61. package/build/hooks/useUninstallState.d.ts.map +1 -1
  62. package/build/hooks/useUninstallState.js +147 -140
  63. package/build/hooks/useUninstallState.js.map +1 -1
  64. package/build/lib/adapter.d.ts.map +1 -1
  65. package/build/lib/adapter.js +3 -3
  66. package/build/lib/adapter.js.map +1 -1
  67. package/build/lib/asset-tools.d.ts +5 -0
  68. package/build/lib/asset-tools.d.ts.map +1 -1
  69. package/build/lib/asset-tools.js +7 -0
  70. package/build/lib/asset-tools.js.map +1 -1
  71. package/build/lib/auth.d.ts.map +1 -1
  72. package/build/lib/auth.js +3 -3
  73. package/build/lib/auth.js.map +1 -1
  74. package/build/lib/checksum.d.ts.map +1 -1
  75. package/build/lib/checksum.js +2 -1
  76. package/build/lib/checksum.js.map +1 -1
  77. package/build/lib/diagnostics.d.ts.map +1 -1
  78. package/build/lib/diagnostics.js +6 -5
  79. package/build/lib/diagnostics.js.map +1 -1
  80. package/build/lib/diff-command.d.ts.map +1 -1
  81. package/build/lib/diff-command.js +10 -6
  82. package/build/lib/diff-command.js.map +1 -1
  83. package/build/lib/entra.d.ts.map +1 -1
  84. package/build/lib/entra.js +3 -2
  85. package/build/lib/entra.js.map +1 -1
  86. package/build/lib/errors.d.ts +9 -0
  87. package/build/lib/errors.d.ts.map +1 -1
  88. package/build/lib/errors.js +29 -0
  89. package/build/lib/errors.js.map +1 -1
  90. package/build/lib/file-snapshot.d.ts +17 -0
  91. package/build/lib/file-snapshot.d.ts.map +1 -0
  92. package/build/lib/file-snapshot.js +41 -0
  93. package/build/lib/file-snapshot.js.map +1 -0
  94. package/build/lib/installer.d.ts +64 -3
  95. package/build/lib/installer.d.ts.map +1 -1
  96. package/build/lib/installer.js +177 -48
  97. package/build/lib/installer.js.map +1 -1
  98. package/build/lib/lockfile.d.ts +23 -4
  99. package/build/lib/lockfile.d.ts.map +1 -1
  100. package/build/lib/lockfile.js +32 -14
  101. package/build/lib/lockfile.js.map +1 -1
  102. package/build/lib/org.d.ts +16 -0
  103. package/build/lib/org.d.ts.map +1 -1
  104. package/build/lib/org.js +18 -0
  105. package/build/lib/org.js.map +1 -1
  106. package/build/lib/owned-paths.d.ts +9 -0
  107. package/build/lib/owned-paths.d.ts.map +1 -0
  108. package/build/lib/owned-paths.js +23 -0
  109. package/build/lib/owned-paths.js.map +1 -0
  110. package/build/lib/publisher.d.ts.map +1 -1
  111. package/build/lib/publisher.js +2 -1
  112. package/build/lib/publisher.js.map +1 -1
  113. package/build/lib/registry.d.ts.map +1 -1
  114. package/build/lib/registry.js +10 -6
  115. package/build/lib/registry.js.map +1 -1
  116. package/build/lib/setup.d.ts.map +1 -1
  117. package/build/lib/setup.js +4 -3
  118. package/build/lib/setup.js.map +1 -1
  119. package/build/lib/sync.d.ts +17 -4
  120. package/build/lib/sync.d.ts.map +1 -1
  121. package/build/lib/sync.js +110 -41
  122. package/build/lib/sync.js.map +1 -1
  123. package/build/lib/uninstaller.d.ts +21 -4
  124. package/build/lib/uninstaller.d.ts.map +1 -1
  125. package/build/lib/uninstaller.js +45 -36
  126. package/build/lib/uninstaller.js.map +1 -1
  127. package/build/lib/updater.d.ts +64 -3
  128. package/build/lib/updater.d.ts.map +1 -1
  129. package/build/lib/updater.js +355 -83
  130. package/build/lib/updater.js.map +1 -1
  131. package/package.json +1 -1
@@ -1,8 +1,14 @@
1
+ import { join } from 'node:path';
1
2
  import { compare, valid } from 'semver';
2
- import { isExcludedForTool } from './asset-tools.js';
3
- import { ApiCodeError, errorGuidance } from './errors.js';
4
- import { executeInstallPlan } from './installer.js';
5
- import { addAssetToLockfile, findDependents, findInstalledAsset, readLockfile, writeLockfile } from './lockfile.js';
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
- * Uninstalls the current version and installs the target from registry.
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
- // Uninstall current version without cascade — new version may still need the same deps.
66
- // The lockfile entry is kept so the reinstall below can carry over the config entries
67
- // other tools registered for this asset (the install replaces the entry either way).
68
- onProgress?.(`Removing ${assetName}@${fromVersion}...`);
69
- await uninstallAsset(assetName, {
70
- adapter,
71
- cascade: false,
72
- keepLockfileEntry: true,
73
- onProgress,
74
- org: installed.org,
75
- projectDir,
76
- });
77
- // Install target version (pass explicit version to resolver)
78
- onProgress?.(`Installing ${assetName}@${toVersion}...`);
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
- results = await executeInstallPlan(planFor(registry, installed, toVersion), {
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
- force: true,
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
- // Install failed — attempt to rollback by re-installing the old version
91
- onProgress?.(`Install of ${assetName}@${toVersion} failed, attempting to restore ${assetName}@${fromVersion}...`);
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 executeInstallPlan(planFor(registry, installed, fromVersion), {
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
- throw new UpdaterError(`Failed to update "${assetName}" to v${toVersion}: ${asSentence(installError)} ` +
103
- `Rollback to v${fromVersion} also failed: ${asSentence(rollbackError)} ` +
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
- throw new UpdaterError(`Failed to update "${assetName}" to v${toVersion}: ${asSentence(installError)} ` +
107
- `The previous version (v${fromVersion}) has been restored.`, errorGuidance(installError));
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
- const directResult = results.find((r) => r.name === assetName);
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
- const updatedLockfile = options.lockfile ?? (await readLockfile(projectDir));
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 = findInstalledAsset(updatedLockfile, dep.name, dep.type, dep.org);
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, dep.name, dep.type);
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
- await uninstallAsset(dep.name, { adapter, cascade: true, onProgress, org: dep.org, projectDir });
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 name = assetNames[0];
151
- const installed = (lockfile.assets ?? []).find((a) => a.name === name);
152
- if (!installed) {
153
- throw new UpdaterError(`Asset "${name}" is not installed.`);
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 ${name}" to unfreeze it before updating.`, {
159
- command: `atk thaw ${name}`,
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 ${name}`,
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 currentLockfile = await readLockfile(options.projectDir);
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
- lockfile: currentLockfile,
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
- toUpdate = outdated.filter((o) => assetNames.includes(o.name));
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 name of assetNames) {
191
- if (!outdated.find((o) => o.name === name)) {
192
- const installed = (lockfile.assets ?? []).find((a) => a.name === name);
193
- if (!installed) {
194
- throw new UpdaterError(`Asset "${name}" is not installed.`);
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${installed.version}).`);
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
- throw new UpdaterError(`Asset "${asset.name}" is frozen. Use "atk thaw ${asset.name}" to unfreeze it before updating.`, {
203
- command: `atk thaw ${asset.name}`,
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.name}`,
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.assets ?? []).find((a) => a.name === asset.name && a.type === asset.type && a.org === asset.org);
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
- for (const asset of toUpdate) {
258
- const installed = (lockfile.assets ?? []).find((a) => a.name === asset.name);
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
- // Read the lockfile once per iteration for dropped dependency cleanup,
264
- // rather than letting each updateAsset call re-read it internally.
265
- const currentLockfile = await readLockfile(options.projectDir);
266
- const result = await updateAsset(asset.name, installed, registry, {
267
- ...options,
268
- lockfile: currentLockfile,
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
- const message = error instanceof Error ? error.message : String(error);
274
- options.onProgress?.(`Failed to update ${asset.name}: ${message}`);
275
- failures.push({ error: message, name: asset.name, type: asset.type, ...errorGuidance(error) });
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
- /** An error's message as a sentence: a period is added only when it doesn't already end with one. */
287
- function asSentence(error) {
288
- const message = error instanceof Error ? error.message : String(error);
289
- return /[.!?]$/.test(message) ? message : `${message}.`;
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.asset.name === installed.name && e.asset.type === installed.type ? { ...e, reason } : 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