@detergent-software/atk 8.1.2-dev.4 → 8.1.2-dev.41

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 (147) hide show
  1. package/CHANGELOG.md +261 -0
  2. package/build/commands/audit.d.ts.map +1 -1
  3. package/build/commands/audit.js +14 -9
  4. package/build/commands/audit.js.map +1 -1
  5. package/build/commands/browse.js +24 -34
  6. package/build/commands/browse.js.map +1 -1
  7. package/build/commands/install.d.ts.map +1 -1
  8. package/build/commands/install.js +32 -73
  9. package/build/commands/install.js.map +1 -1
  10. package/build/commands/list.js +2 -2
  11. package/build/commands/list.js.map +1 -1
  12. package/build/commands/login.js +2 -2
  13. package/build/commands/login.js.map +1 -1
  14. package/build/commands/register.js +3 -3
  15. package/build/commands/register.js.map +1 -1
  16. package/build/commands/sync.d.ts.map +1 -1
  17. package/build/commands/sync.js +16 -11
  18. package/build/commands/sync.js.map +1 -1
  19. package/build/commands/uninstall.d.ts +9 -1
  20. package/build/commands/uninstall.d.ts.map +1 -1
  21. package/build/commands/uninstall.js +23 -26
  22. package/build/commands/uninstall.js.map +1 -1
  23. package/build/commands/update.d.ts +9 -1
  24. package/build/commands/update.d.ts.map +1 -1
  25. package/build/commands/update.js +54 -14
  26. package/build/commands/update.js.map +1 -1
  27. package/build/components/InstallConfirmBar.d.ts.map +1 -1
  28. package/build/components/InstallConfirmBar.js +2 -1
  29. package/build/components/InstallConfirmBar.js.map +1 -1
  30. package/build/components/ListActionBar.d.ts.map +1 -1
  31. package/build/components/ListActionBar.js +3 -2
  32. package/build/components/ListActionBar.js.map +1 -1
  33. package/build/components/RunStoppedHint.d.ts +13 -0
  34. package/build/components/RunStoppedHint.d.ts.map +1 -0
  35. package/build/components/RunStoppedHint.js +12 -0
  36. package/build/components/RunStoppedHint.js.map +1 -0
  37. package/build/components/SkippedAssetSection.d.ts +2 -0
  38. package/build/components/SkippedAssetSection.d.ts.map +1 -1
  39. package/build/components/SkippedAssetSection.js +1 -1
  40. package/build/components/SkippedAssetSection.js.map +1 -1
  41. package/build/components/SyncDriftedList.d.ts +3 -2
  42. package/build/components/SyncDriftedList.d.ts.map +1 -1
  43. package/build/components/SyncDriftedList.js +11 -3
  44. package/build/components/SyncDriftedList.js.map +1 -1
  45. package/build/components/index.d.ts +1 -0
  46. package/build/components/index.d.ts.map +1 -1
  47. package/build/components/index.js +1 -0
  48. package/build/components/index.js.map +1 -1
  49. package/build/hooks/useCommand.js +2 -2
  50. package/build/hooks/useCommand.js.map +1 -1
  51. package/build/hooks/useInitState.js +5 -5
  52. package/build/hooks/useInitState.js.map +1 -1
  53. package/build/hooks/useListActions.d.ts.map +1 -1
  54. package/build/hooks/useListActions.js +34 -18
  55. package/build/hooks/useListActions.js.map +1 -1
  56. package/build/hooks/usePruneState.js +9 -9
  57. package/build/hooks/usePruneState.js.map +1 -1
  58. package/build/hooks/usePublishState.js +3 -3
  59. package/build/hooks/usePublishState.js.map +1 -1
  60. package/build/hooks/useRegistryChangeState.js +2 -2
  61. package/build/hooks/useRegistryChangeState.js.map +1 -1
  62. package/build/hooks/useSetupState.js +3 -3
  63. package/build/hooks/useSetupState.js.map +1 -1
  64. package/build/hooks/useUninstallState.d.ts +16 -29
  65. package/build/hooks/useUninstallState.d.ts.map +1 -1
  66. package/build/hooks/useUninstallState.js +147 -140
  67. package/build/hooks/useUninstallState.js.map +1 -1
  68. package/build/lib/adapter.d.ts.map +1 -1
  69. package/build/lib/adapter.js +3 -3
  70. package/build/lib/adapter.js.map +1 -1
  71. package/build/lib/asset-tools.d.ts +5 -0
  72. package/build/lib/asset-tools.d.ts.map +1 -1
  73. package/build/lib/asset-tools.js +7 -0
  74. package/build/lib/asset-tools.js.map +1 -1
  75. package/build/lib/auth.d.ts.map +1 -1
  76. package/build/lib/auth.js +3 -3
  77. package/build/lib/auth.js.map +1 -1
  78. package/build/lib/checksum.d.ts.map +1 -1
  79. package/build/lib/checksum.js +2 -1
  80. package/build/lib/checksum.js.map +1 -1
  81. package/build/lib/diagnostics.d.ts.map +1 -1
  82. package/build/lib/diagnostics.js +6 -5
  83. package/build/lib/diagnostics.js.map +1 -1
  84. package/build/lib/diff-command.d.ts.map +1 -1
  85. package/build/lib/diff-command.js +10 -6
  86. package/build/lib/diff-command.js.map +1 -1
  87. package/build/lib/enabled-list.d.ts +15 -0
  88. package/build/lib/enabled-list.d.ts.map +1 -0
  89. package/build/lib/enabled-list.js +83 -0
  90. package/build/lib/enabled-list.js.map +1 -0
  91. package/build/lib/entra.d.ts.map +1 -1
  92. package/build/lib/entra.js +3 -2
  93. package/build/lib/entra.js.map +1 -1
  94. package/build/lib/errors.d.ts +9 -0
  95. package/build/lib/errors.d.ts.map +1 -1
  96. package/build/lib/errors.js +29 -0
  97. package/build/lib/errors.js.map +1 -1
  98. package/build/lib/file-snapshot.d.ts +17 -0
  99. package/build/lib/file-snapshot.d.ts.map +1 -0
  100. package/build/lib/file-snapshot.js +41 -0
  101. package/build/lib/file-snapshot.js.map +1 -0
  102. package/build/lib/installed-for-tool.d.ts +8 -0
  103. package/build/lib/installed-for-tool.d.ts.map +1 -0
  104. package/build/lib/installed-for-tool.js +18 -0
  105. package/build/lib/installed-for-tool.js.map +1 -0
  106. package/build/lib/installer.d.ts +108 -4
  107. package/build/lib/installer.d.ts.map +1 -1
  108. package/build/lib/installer.js +551 -168
  109. package/build/lib/installer.js.map +1 -1
  110. package/build/lib/lockfile.d.ts +23 -4
  111. package/build/lib/lockfile.d.ts.map +1 -1
  112. package/build/lib/lockfile.js +32 -14
  113. package/build/lib/lockfile.js.map +1 -1
  114. package/build/lib/multi-tool-install.d.ts +23 -0
  115. package/build/lib/multi-tool-install.d.ts.map +1 -0
  116. package/build/lib/multi-tool-install.js +78 -0
  117. package/build/lib/multi-tool-install.js.map +1 -0
  118. package/build/lib/org.d.ts +16 -0
  119. package/build/lib/org.d.ts.map +1 -1
  120. package/build/lib/org.js +18 -0
  121. package/build/lib/org.js.map +1 -1
  122. package/build/lib/owned-paths.d.ts +68 -0
  123. package/build/lib/owned-paths.d.ts.map +1 -0
  124. package/build/lib/owned-paths.js +162 -0
  125. package/build/lib/owned-paths.js.map +1 -0
  126. package/build/lib/publisher.d.ts.map +1 -1
  127. package/build/lib/publisher.js +2 -1
  128. package/build/lib/publisher.js.map +1 -1
  129. package/build/lib/registry.d.ts.map +1 -1
  130. package/build/lib/registry.js +10 -6
  131. package/build/lib/registry.js.map +1 -1
  132. package/build/lib/setup.d.ts.map +1 -1
  133. package/build/lib/setup.js +4 -3
  134. package/build/lib/setup.js.map +1 -1
  135. package/build/lib/sync.d.ts +17 -4
  136. package/build/lib/sync.d.ts.map +1 -1
  137. package/build/lib/sync.js +119 -45
  138. package/build/lib/sync.js.map +1 -1
  139. package/build/lib/uninstaller.d.ts +21 -4
  140. package/build/lib/uninstaller.d.ts.map +1 -1
  141. package/build/lib/uninstaller.js +61 -91
  142. package/build/lib/uninstaller.js.map +1 -1
  143. package/build/lib/updater.d.ts +64 -3
  144. package/build/lib/updater.d.ts.map +1 -1
  145. package/build/lib/updater.js +367 -83
  146. package/build/lib/updater.js.map +1 -1
  147. 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, {
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, {
82
156
  adapter,
83
- force: true,
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, 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}...`);
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, lockfile, await recordedLockfile(options), 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,149 @@ 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 = [];
373
+ const recorded = await recordedLockfile(options);
237
374
  toUpdate = toUpdate.filter((asset) => {
238
- const installed = (lockfile.assets ?? []).find((a) => a.name === asset.name && a.type === asset.type && a.org === asset.org);
375
+ const installed = lockfileEntryFor(lockfile, asset);
239
376
  if (installed && isExcludedForTool(installed, options.adapter.tool)) {
240
377
  options.onProgress?.(`Skipping ${asset.name}: uninstalled for ${options.adapter.tool}`);
241
378
  excludedSkipped.push(toExcludedSkipped(installed));
242
379
  return false;
243
380
  }
381
+ const failedSkip = failedSkipFor(asset, lockfile, recorded, options);
382
+ if (failedSkip) {
383
+ failedSkipped.push(failedSkip);
384
+ return false;
385
+ }
386
+ // The tool has no placement for the type (a rule under Codex CLI): nothing to update.
387
+ if (!supportsType(options.adapter, asset.type)) {
388
+ reportUnsupported(asset, options);
389
+ return false;
390
+ }
244
391
  return true;
245
392
  });
246
393
  if (toUpdate.length === 0) {
247
394
  return {
248
395
  ...(excludedSkipped.length > 0 ? { excludedSkipped } : {}),
396
+ ...(failedSkipped.length > 0 ? { failedSkipped } : {}),
249
397
  failures: [],
250
398
  frozenSkipped: frozenSkipped.length > 0 ? frozenSkipped : undefined,
251
399
  results: [],
252
400
  skipped: skipped.length > 0 ? skipped : undefined,
253
401
  };
254
402
  }
403
+ // Resolve the credential once, up front. A missing or expired sign-in carries no API code for
404
+ // isRunWideFailure, and with a fresh registry cache it would otherwise fail each asset's
405
+ // download in turn.
406
+ await getCredential();
255
407
  const results = [];
256
408
  const failures = [];
257
- for (const asset of toUpdate) {
258
- const installed = (lockfile.assets ?? []).find((a) => a.name === asset.name);
409
+ let notAttempted;
410
+ // What this tool has: the lockfile the run started from, plus what this tool's updates install.
411
+ let toolLockfile = options.toolLockfile ?? lockfile;
412
+ for (const [index, asset] of toUpdate.entries()) {
413
+ const installed = lockfileEntryFor(lockfile, asset);
259
414
  if (!installed) {
260
415
  throw new UpdaterError(`Asset "${asset.name}" is installed but not found in lockfile.`);
261
416
  }
417
+ let result;
262
418
  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);
419
+ const dependencySkip = await failedDependencySkipFor(installed, registry, options);
420
+ if (dependencySkip) {
421
+ failedSkipped.push(dependencySkip);
422
+ continue;
423
+ }
424
+ result = await updateAsset(asset.name, installed, registry, { ...options, toolLockfile });
271
425
  }
272
426
  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) });
427
+ // Most errors from updateAsset already name the asset; only frame the ones that don't.
428
+ const cause = errorMessage(error);
429
+ const message = cause.includes(`"${asset.name}"`)
430
+ ? cause
431
+ : `Failed to update "${asset.name}": ${asSentence(cause)}`;
432
+ options.onProgress?.(message);
433
+ const guidance = errorGuidance(error);
434
+ failures.push({
435
+ error: message,
436
+ name: asset.name,
437
+ ...(asset.org ? { org: asset.org } : {}),
438
+ type: asset.type,
439
+ ...guidance,
440
+ });
441
+ // The same error would fail every remaining asset.
442
+ if (isRunWideFailure(guidance)) {
443
+ notAttempted = toUpdate.slice(index + 1).map((a) => a.name);
444
+ break;
445
+ }
446
+ continue;
276
447
  }
448
+ results.push(result);
449
+ // The update succeeded either way; a failed read leaves the next update to report it.
450
+ toolLockfile = await withInstalledEntries(toolLockfile, planFor(registry, installed, result.toVersion), options).catch(() => toolLockfile);
277
451
  }
278
452
  return {
279
453
  ...(excludedSkipped.length > 0 ? { excludedSkipped } : {}),
454
+ ...(failedSkipped.length > 0 ? { failedSkipped } : {}),
280
455
  failures,
281
456
  frozenSkipped: frozenSkipped.length > 0 ? frozenSkipped : undefined,
457
+ ...(notAttempted ? { notAttempted } : {}),
282
458
  results,
283
459
  skipped: skipped.length > 0 ? skipped : undefined,
284
460
  };
285
461
  }
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}.`;
462
+ /** How a command names an asset: `@org/name`, with `--type` when the run was given one. */
463
+ function assetSpec(asset, type) {
464
+ return `${formatOrgName(asset.org, asset.name)}${type ? ` --type ${asset.type}` : ''}`;
465
+ }
466
+ /**
467
+ * The skip record for an asset whose update would move a dependency whose own update failed for
468
+ * an earlier tool in the run: that tool kept the version the lockfile records, so moving it here
469
+ * would leave the tools on different versions. A dependency the lockfile already records at the
470
+ * plan's version, or one this tool has no placement for, is not moved, so it does not count.
471
+ */
472
+ async function failedDependencySkipFor(installed, registry, options) {
473
+ if (!options.failedFor?.size)
474
+ return undefined;
475
+ // An asset that does not resolve gets its error from the update itself.
476
+ const resolved = findAsset(registry, installed.name, installed.type, options.targetVersion, installed.org);
477
+ if (!resolved || resolved.asset.org !== installed.org)
478
+ return undefined;
479
+ const lockfile = await readLockfile(options.projectDir);
480
+ for (const { asset, version } of planFor(registry, installed, resolved.version).entries) {
481
+ if (isSameAsset(asset, installed) || !supportsType(options.adapter, asset.type))
482
+ continue;
483
+ const failedFor = options.failedFor.get(`${asset.type}:${asset.org ?? ''}:${asset.name}`);
484
+ if (!failedFor || findInstalledAsset(lockfile, asset.name, asset.type, asset.org)?.version === version)
485
+ continue;
486
+ options.onProgress?.(`Skipping ${installed.name} for ${options.adapter.tool}: its dependency ${asset.name} failed to update for ${failedFor}`);
487
+ return {
488
+ dependency: { name: asset.name, ...(asset.org ? { org: asset.org } : {}), type: asset.type },
489
+ failedFor,
490
+ name: installed.name,
491
+ ...(installed.org ? { org: installed.org } : {}),
492
+ type: installed.type,
493
+ };
494
+ }
495
+ return undefined;
496
+ }
497
+ /**
498
+ * The skip record for an asset whose update failed for an earlier tool in the run, if it did. An
499
+ * asset that another asset's plan moved for that tool after all (the lockfile no longer records
500
+ * the run's starting version) is not skipped: the later tools move it too.
501
+ */
502
+ function failedSkipFor(asset, snapshot, recorded, options) {
503
+ const failedFor = options.failedFor?.get(`${asset.type}:${asset.org ?? ''}:${asset.name}`);
504
+ if (!failedFor)
505
+ return undefined;
506
+ const versionIn = (lockfile) => lockfile && findInstalledAsset(lockfile, asset.name, asset.type, asset.org)?.version;
507
+ if (versionIn(recorded) !== versionIn(snapshot))
508
+ return undefined;
509
+ options.onProgress?.(`Skipping ${asset.name} for ${options.adapter.tool}: its update failed for ${failedFor}`);
510
+ return { failedFor, name: asset.name, ...(asset.org ? { org: asset.org } : {}), type: asset.type };
511
+ }
512
+ /** Why an install for an update failed, as a sentence; the update message already names the asset. */
513
+ function failureReason(error, installed) {
514
+ return asSentence(installFailureMessage(error, installed));
290
515
  }
291
516
  /**
292
517
  * Check if the registry version is newer than the installed version.
@@ -301,19 +526,35 @@ function isNewer(registryVersion, installedVersion) {
301
526
  // silently ignored.
302
527
  return registryVersion !== installedVersion;
303
528
  }
529
+ function isSameAsset(a, b) {
530
+ return a.name === b.name && a.type === b.type && a.org === b.org;
531
+ }
532
+ /** The lockfile entry of an outdated asset: same name, type and org. */
533
+ function lockfileEntryFor(lockfile, asset) {
534
+ return (lockfile.assets ?? []).find((a) => isSameAsset(a, asset));
535
+ }
304
536
  /**
305
537
  * The install plan for one version of an installed asset. The resolver treats the asset as
306
538
  * 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.
539
+ * dependency must not turn it into a direct install. Only the requested asset's entry is
540
+ * `direct`, so a same-named entry, such as a dependency on another version of the asset
541
+ * itself, keeps its own reason.
308
542
  */
309
543
  function planFor(registry, installed, version) {
310
544
  const plan = resolveInstallPlan(registry, installed.name, installed.type, version, installed.org);
311
545
  const reason = installed.installReason ?? 'direct';
312
546
  return {
313
547
  ...plan,
314
- entries: plan.entries.map((e) => e.asset.name === installed.name && e.asset.type === installed.type ? { ...e, reason } : e),
548
+ entries: plan.entries.map((e) => (e.reason === 'direct' ? { ...e, reason } : e)),
315
549
  };
316
550
  }
551
+ /** The lockfile on disk, read only when an earlier tool in the run had a failed update. */
552
+ async function recordedLockfile(options) {
553
+ return options.failedFor?.size ? readLockfile(options.projectDir) : undefined;
554
+ }
555
+ function reportUnsupported(asset, options) {
556
+ options.onProgress?.(`Skipping ${asset.name}: ${options.adapter.tool} has no placement for ${asset.type} assets`);
557
+ }
317
558
  /**
318
559
  * Put a bundle member's bundle origin back on its updated lockfile entry. Passing it to
319
560
  * executeInstallPlan would also stamp the asset's dependencies, so it is set here instead.
@@ -327,7 +568,50 @@ async function restoreBundleOrigin(projectDir, installed) {
327
568
  return;
328
569
  await writeLockfile(projectDir, addAssetToLockfile(lockfile, { ...entry, bundleOrigin: installed.bundleOrigin }));
329
570
  }
571
+ function scopeSwitchError(installed, fallback) {
572
+ const { message, ...guidance } = scopeSwitchGuidance(installed, fallback, 'update');
573
+ return new UpdaterError(message, guidance);
574
+ }
330
575
  function toExcludedSkipped(installed) {
331
576
  return { name: installed.name, ...(installed.org ? { org: installed.org } : {}), type: installed.type };
332
577
  }
578
+ /**
579
+ * The absolute paths that updating `installed` with `plan` can change for the adapter's tool:
580
+ * the lockfile, the placed files and config files of the asset and of every installed asset the
581
+ * plan replaces (its dependencies), and the config files the plan's new assets register in. Each
582
+ * entry of `installed` and each lockfile of `lockfiles` is a view of what is installed; all count.
583
+ */
584
+ function updateTouchedPaths(lockfiles, plan, installed, adapter, projectDir) {
585
+ const relativePaths = [];
586
+ const replaced = lockfiles.flatMap((lockfile) => plan.entries.flatMap((e) => findInstalledAsset(lockfile, e.asset.name, e.asset.type, e.asset.org) ?? []));
587
+ for (const asset of [...installed, ...replaced]) {
588
+ const resolved = resolveInstalledPaths(asset, adapter, projectDir);
589
+ relativePaths.push(...resolved.files.map((f) => f.installedPath));
590
+ relativePaths.push(...[...getConfigEntriesForTool(asset, adapter), ...resolved.configEntries].map((e) => e.file));
591
+ }
592
+ for (const { asset } of plan.entries) {
593
+ const placement = getPlacement(adapter, asset.type);
594
+ if (!placement)
595
+ continue;
596
+ if (placement.configFile)
597
+ relativePaths.push(placement.configFile);
598
+ if (placement.enabledListFile)
599
+ relativePaths.push(placement.enabledListFile);
600
+ // A memory template is spliced into a shared file at its placement path.
601
+ if (asset.type === 'memory-template')
602
+ relativePaths.push(placement.path.replace(/\{name\}/g, asset.name));
603
+ }
604
+ return [getLockfilePath(projectDir), ...relativePaths.map((p) => join(projectDir, p))];
605
+ }
606
+ /** `toolLockfile` with the on-disk lockfile entries of the assets an update installed with `plan`. */
607
+ async function withInstalledEntries(toolLockfile, plan, { projectDir }) {
608
+ const lockfile = await readLockfile(projectDir);
609
+ let updated = toolLockfile;
610
+ for (const { asset } of plan.entries) {
611
+ const entry = findInstalledAsset(lockfile, asset.name, asset.type, asset.org);
612
+ if (entry)
613
+ updated = addAssetToLockfile(updated, entry);
614
+ }
615
+ return updated;
616
+ }
333
617
  //# sourceMappingURL=updater.js.map